From f213dea529adfc4fd1c6b9289492f18d587ed21d Mon Sep 17 00:00:00 2001 From: scossar Date: Tue, 20 Mar 2018 12:13:41 -0700 Subject: [PATCH 001/287] Make sure a post has replies before accessing the reply id --- app/views/embed/comments.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/embed/comments.html.erb b/app/views/embed/comments.html.erb index 23f69584fe..b6863c21aa 100644 --- a/app/views/embed/comments.html.erb +++ b/app/views/embed/comments.html.erb @@ -27,7 +27,7 @@ <%= get_html(post.cooked) %> - <%- if post.reply_count > 0 %> + <%- if post.reply_count > 0 && post.replies.exists? %> <%- if post.reply_count == 1 %> <%= link_to I18n.t('embed.replies', count: post.reply_count), post.full_url, 'data-link-to-post' => post.replies.first.id.to_s, :class => 'post-replies button' %> <% else %> From ced7e9a691ab09ea926aa7869ee757d8e60b87ac Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Thu, 15 Mar 2018 17:10:45 -0400 Subject: [PATCH 002/287] FEATURE: control which web crawlers can access using a whitelist or blacklist --- .../javascripts/admin/models/report.js.es6 | 72 +++++++---- .../components/admin-table-report.hbs | 17 +++ .../javascripts/admin/templates/reports.hbs | 18 +-- .../stylesheets/common/admin/admin_base.scss | 14 +++ app/controllers/robots_txt_controller.rb | 16 ++- app/jobs/scheduled/clean_up_crawler_stats.rb | 26 ++++ app/models/application_request.rb | 40 +----- app/models/concerns/cached_counting.rb | 67 ++++++++++ app/models/report.rb | 14 ++- app/models/web_crawler_request.rb | 76 ++++++++++++ app/views/robots_txt/index.erb | 12 +- config/locales/server.en.yml | 6 + config/site_settings.yml | 6 + ...80320190339_create_web_crawler_requests.rb | 11 ++ lib/crawler_detection.rb | 21 ++++ lib/middleware/request_tracker.rb | 35 ++++-- spec/components/crawler_detection_spec.rb | 111 ++++++++++++++++- .../middleware/request_tracker_spec.rb | 46 +++++++ .../web_crawler_request_fabricator.rb | 5 + spec/jobs/clean_up_crawler_stats_spec.rb | 31 +++++ spec/models/web_crawler_request_spec.rb | 114 ++++++++++++++++++ spec/requests/robots_txt_controller_spec.rb | 61 +++++++++- 22 files changed, 722 insertions(+), 97 deletions(-) create mode 100644 app/assets/javascripts/admin/templates/components/admin-table-report.hbs create mode 100644 app/jobs/scheduled/clean_up_crawler_stats.rb create mode 100644 app/models/concerns/cached_counting.rb create mode 100644 app/models/web_crawler_request.rb create mode 100644 db/migrate/20180320190339_create_web_crawler_requests.rb create mode 100644 spec/fabricators/web_crawler_request_fabricator.rb create mode 100644 spec/jobs/clean_up_crawler_stats_spec.rb create mode 100644 spec/models/web_crawler_request_spec.rb diff --git a/app/assets/javascripts/admin/models/report.js.es6 b/app/assets/javascripts/admin/models/report.js.es6 index af078528b7..14e059297e 100644 --- a/app/assets/javascripts/admin/models/report.js.es6 +++ b/app/assets/javascripts/admin/models/report.js.es6 @@ -2,6 +2,7 @@ import { ajax } from 'discourse/lib/ajax'; import round from "discourse/lib/round"; import { fmt } from 'discourse/lib/computed'; import { fillMissingDates } from 'discourse/lib/utilities'; +import computed from 'ember-addons/ember-computed-decorators'; const Report = Discourse.Model.extend({ reportUrl: fmt("type", "/admin/reports/%@"), @@ -42,7 +43,8 @@ const Report = Discourse.Model.extend({ lastSevenDaysCount: function() { return this.valueFor(1, 7); }.property("data"), lastThirtyDaysCount: function() { return this.valueFor(1, 30); }.property("data"), - yesterdayTrend: function() { + @computed('data') + yesterdayTrend() { const yesterdayVal = this.valueAt(1); const twoDaysAgoVal = this.valueAt(2); if (yesterdayVal > twoDaysAgoVal) { @@ -52,9 +54,10 @@ const Report = Discourse.Model.extend({ } else { return "no-change"; } - }.property("data"), + }, - sevenDayTrend: function() { + @computed('data') + sevenDayTrend() { const currentPeriod = this.valueFor(1, 7); const prevPeriod = this.valueFor(8, 14); if (currentPeriod > prevPeriod) { @@ -64,36 +67,39 @@ const Report = Discourse.Model.extend({ } else { return "no-change"; } - }.property("data"), + }, - thirtyDayTrend: function() { - if (this.get("prev30Days")) { + @computed('prev30Days', 'data') + thirtyDayTrend(prev30Days) { + if (prev30Days) { const currentPeriod = this.valueFor(1, 30); if (currentPeriod > this.get("prev30Days")) { return "trending-up"; - } else if (currentPeriod < this.get("prev30Days")) { + } else if (currentPeriod < prev30Days) { return "trending-down"; } } return "no-change"; - }.property("data", "prev30Days"), + }, - icon: function() { - switch (this.get("type")) { + @computed('type') + icon(type) { + switch (type) { case "flags": return "flag"; case "likes": return "heart"; case "bookmarks": return "bookmark"; default: return null; } - }.property("type"), + }, - method: function() { - if (this.get("type") === "time_to_first_response") { + @computed('type') + method(type) { + if (type === "time_to_first_response") { return "average"; } else { return "sum"; } - }.property("type"), + }, percentChangeString(val1, val2) { const val = ((val1 - val2) / val2) * 100; @@ -114,21 +120,31 @@ const Report = Discourse.Model.extend({ return title; }, - yesterdayCountTitle: function() { + @computed('data') + yesterdayCountTitle() { return this.changeTitle(this.valueAt(1), this.valueAt(2), "two days ago"); - }.property("data"), + }, - sevenDayCountTitle: function() { + @computed('data') + sevenDayCountTitle() { return this.changeTitle(this.valueFor(1, 7), this.valueFor(8, 14), "two weeks ago"); - }.property("data"), + }, - thirtyDayCountTitle: function() { - return this.changeTitle(this.valueFor(1, 30), this.get("prev30Days"), "in the previous 30 day period"); - }.property("data"), + @computed('prev30Days', 'data') + thirtyDayCountTitle(prev30Days) { + return this.changeTitle(this.valueFor(1, 30), prev30Days, "in the previous 30 day period"); + }, - dataReversed: function() { - return this.get("data").toArray().reverse(); - }.property("data") + @computed('data') + sortedData(data) { + return this.get('xaxisIsDate') ? data.toArray().reverse() : data.toArray(); + }, + + @computed('data') + xaxisIsDate() { + if (!this.data[0]) return false; + return this.data && moment(this.data[0].x, 'YYYY-MM-DD').isValid(); + } }); @@ -152,6 +168,14 @@ Report.reopenClass({ const model = Report.create({ type: type }); model.setProperties(json.report); + + if (json.report.related_report) { + // TODO: fillMissingDates if xaxis is date + const related = Report.create({ type: json.report.related_report.type }); + related.setProperties(json.report.related_report); + model.set('relatedReport', related); + } + return model; }); } diff --git a/app/assets/javascripts/admin/templates/components/admin-table-report.hbs b/app/assets/javascripts/admin/templates/components/admin-table-report.hbs new file mode 100644 index 0000000000..53ea2e51c6 --- /dev/null +++ b/app/assets/javascripts/admin/templates/components/admin-table-report.hbs @@ -0,0 +1,17 @@ +{{#if model.sortedData}} + + + + + + + {{#each model.sortedData as |row|}} + + + + + {{/each}} +
{{model.xaxis}}{{model.yaxis}}
{{row.x}} + {{row.y}} +
+{{/if}} diff --git a/app/assets/javascripts/admin/templates/reports.hbs b/app/assets/javascripts/admin/templates/reports.hbs index 3927d1e384..1a6b4bf047 100644 --- a/app/assets/javascripts/admin/templates/reports.hbs +++ b/app/assets/javascripts/admin/templates/reports.hbs @@ -31,20 +31,10 @@ {{#if viewingGraph}} {{admin-graph model=model}} {{else}} - - - - - + {{admin-table-report model=model}} + {{/if}} - {{#each model.dataReversed as |row|}} - - - - - {{/each}} -
{{model.xaxis}}{{model.yaxis}}
{{row.x}} - {{row.y}} -
+ {{#if model.relatedReport}} + {{admin-table-report model=model.relatedReport}} {{/if}} {{/conditional-loading-spinner}} diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index 122a647925..04708b6e64 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -167,6 +167,20 @@ $mobile-breakpoint: 700px; } } + &.web_crawlers { + tr { + th:nth-of-type(1) { + width: 60%; + } + } + td.x-value { + max-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + .bar-container { float: left; width: 300px; diff --git a/app/controllers/robots_txt_controller.rb b/app/controllers/robots_txt_controller.rb index 4a8600774c..2315120935 100644 --- a/app/controllers/robots_txt_controller.rb +++ b/app/controllers/robots_txt_controller.rb @@ -3,7 +3,21 @@ class RobotsTxtController < ApplicationController skip_before_action :preload_json, :check_xhr, :redirect_to_login_if_required def index - path = SiteSetting.allow_index_in_robots_txt ? :index : :no_index + if SiteSetting.allow_index_in_robots_txt + path = :index + if SiteSetting.whitelisted_crawler_user_agents.present? + @allowed_user_agents = SiteSetting.whitelisted_crawler_user_agents.split('|') + @disallowed_user_agents = ['*'] + elsif SiteSetting.blacklisted_crawler_user_agents.present? + @allowed_user_agents = ['*'] + @disallowed_user_agents = SiteSetting.blacklisted_crawler_user_agents.split('|') + else + @allowed_user_agents = ['*'] + end + else + path = :no_index + end + render path, content_type: 'text/plain' end end diff --git a/app/jobs/scheduled/clean_up_crawler_stats.rb b/app/jobs/scheduled/clean_up_crawler_stats.rb new file mode 100644 index 0000000000..816e91f0fd --- /dev/null +++ b/app/jobs/scheduled/clean_up_crawler_stats.rb @@ -0,0 +1,26 @@ +module Jobs + + class CleanUpCrawlerStats < Jobs::Scheduled + every 1.day + + def execute(args) + WebCrawlerRequest.where('date < ?', WebCrawlerRequest.max_record_age.ago).delete_all + + # keep count of only the top user agents + WebCrawlerRequest.exec_sql <<~SQL + WITH ranked_requests AS ( + SELECT row_number() OVER (ORDER BY count DESC) as row_number, id + FROM web_crawler_requests + WHERE date = '#{1.day.ago.strftime("%Y-%m-%d")}' + ) + DELETE FROM web_crawler_requests + WHERE id IN ( + SELECT ranked_requests.id + FROM ranked_requests + WHERE row_number > #{WebCrawlerRequest.max_records_per_day} + ) + SQL + end + end + +end diff --git a/app/models/application_request.rb b/app/models/application_request.rb index f13e7275a7..1e4459f681 100644 --- a/app/models/application_request.rb +++ b/app/models/application_request.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true class ApplicationRequest < ActiveRecord::Base + enum req_type: %i(http_total http_2xx http_background @@ -12,41 +13,12 @@ class ApplicationRequest < ActiveRecord::Base page_view_logged_in_mobile page_view_anon_mobile) - cattr_accessor :autoflush, :autoflush_seconds, :last_flush - # auto flush if backlog is larger than this - self.autoflush = 2000 - - # auto flush if older than this - self.autoflush_seconds = 5.minutes - self.last_flush = Time.now.utc + include CachedCounting def self.increment!(type, opts = nil) - key = redis_key(type) - val = $redis.incr(key).to_i - - # readonly mode it is going to be 0, skip - return if val == 0 - - # 3.days, see: https://github.com/rails/rails/issues/21296 - $redis.expire(key, 259200) - - autoflush = (opts && opts[:autoflush]) || self.autoflush - if autoflush > 0 && val >= autoflush - write_cache! - return - end - - if (Time.now.utc - last_flush).to_i > autoflush_seconds - write_cache! - end + perform_increment!(redis_key(type), opts) end - GET_AND_RESET = <<~LUA - local val = redis.call('get', KEYS[1]) - redis.call('set', KEYS[1], '0') - return val - LUA - def self.write_cache!(date = nil) if date.nil? write_cache!(Time.now.utc) @@ -58,13 +30,9 @@ class ApplicationRequest < ActiveRecord::Base date = date.to_date - # this may seem a bit fancy but in so it allows - # for concurrent calls without double counting req_types.each do |req_type, _| - key = redis_key(req_type, date) + val = get_and_reset(redis_key(req_type, date)) - namespaced_key = $redis.namespace_key(key) - val = $redis.without_namespace.eval(GET_AND_RESET, keys: [namespaced_key]).to_i next if val == 0 id = req_id(date, req_type) diff --git a/app/models/concerns/cached_counting.rb b/app/models/concerns/cached_counting.rb new file mode 100644 index 0000000000..8f629df70e --- /dev/null +++ b/app/models/concerns/cached_counting.rb @@ -0,0 +1,67 @@ +module CachedCounting + extend ActiveSupport::Concern + + included do + class << self + attr_accessor :autoflush, :autoflush_seconds, :last_flush + end + + # auto flush if backlog is larger than this + self.autoflush = 2000 + + # auto flush if older than this + self.autoflush_seconds = 5.minutes + + self.last_flush = Time.now.utc + end + + class_methods do + def perform_increment!(key, opts = nil) + val = $redis.incr(key).to_i + + # readonly mode it is going to be 0, skip + return if val == 0 + + # 3.days, see: https://github.com/rails/rails/issues/21296 + $redis.expire(key, 259200) + + autoflush = (opts && opts[:autoflush]) || self.autoflush + if autoflush > 0 && val >= autoflush + write_cache! + return + end + + if (Time.now.utc - last_flush).to_i > autoflush_seconds + write_cache! + end + end + + def write_cache!(date = nil) + raise NotImplementedError + end + + GET_AND_RESET = <<~LUA + local val = redis.call('get', KEYS[1]) + redis.call('set', KEYS[1], '0') + return val + LUA + + # this may seem a bit fancy but in so it allows + # for concurrent calls without double counting + def get_and_reset(key) + namespaced_key = $redis.namespace_key(key) + $redis.without_namespace.eval(GET_AND_RESET, keys: [namespaced_key]).to_i + end + + def request_id(query_params, retries = 0) + id = where(query_params).pluck(:id).first + id ||= create!(query_params.merge(count: 0)).id + rescue # primary key violation + if retries == 0 + request_id(query_params, 1) + else + raise + end + end + end +end diff --git a/app/models/report.rb b/app/models/report.rb index 04f27453a0..151ea28c47 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -27,7 +27,11 @@ class Report category_id: category_id, group_id: group_id, prev30Days: self.prev30Days - } + }.tap do |json| + if type == 'page_view_crawler_reqs' + json[:related_report] = Report.find('web_crawlers', start_date: start_date, end_date: end_date)&.as_json + end + end end def Report.add_report(name, &block) @@ -225,4 +229,12 @@ class Report def self.report_notify_user_private_messages(report) private_messages_report report, TopicSubtype.notify_user end + + def self.report_web_crawlers(report) + report.data = WebCrawlerRequest.where('date >= ? and date <= ?', report.start_date, report.end_date) + .limit(200) + .order('sum_count DESC') + .group(:user_agent).sum(:count) + .map { |ua, count| { x: ua, y: count } } + end end diff --git a/app/models/web_crawler_request.rb b/app/models/web_crawler_request.rb new file mode 100644 index 0000000000..3362259905 --- /dev/null +++ b/app/models/web_crawler_request.rb @@ -0,0 +1,76 @@ +class WebCrawlerRequest < ActiveRecord::Base + include CachedCounting + + # auto flush if older than this + self.autoflush_seconds = 1.hour + + cattr_accessor :max_record_age, :max_records_per_day + + # only keep the top records based on request count + self.max_records_per_day = 200 + + # delete records older than this + self.max_record_age = 30.days + + def self.increment!(user_agent, opts = nil) + ua_list_key = user_agent_list_key + $redis.sadd(ua_list_key, user_agent) + $redis.expire(ua_list_key, 259200) # 3.days + + perform_increment!(redis_key(user_agent), opts) + end + + def self.write_cache!(date = nil) + if date.nil? + write_cache!(Time.now.utc) + write_cache!(Time.now.utc.yesterday) + return + end + + self.last_flush = Time.now.utc + + date = date.to_date + + $redis.smembers(user_agent_list_key(date)).each do |user_agent, _| + + val = get_and_reset(redis_key(user_agent, date)) + + next if val == 0 + + self.where(id: req_id(date, user_agent)).update_all(["count = count + ?", val]) + end + rescue Redis::CommandError => e + raise unless e.message =~ /READONLY/ + nil + end + + def self.clear_cache!(date = nil) + if date.nil? + clear_cache!(Time.now.utc) + clear_cache!(Time.now.utc.yesterday) + return + end + + list_key = user_agent_list_key(date) + + $redis.smembers(list_key).each do |user_agent, _| + $redis.del redis_key(user_agent, date) + end + + $redis.del list_key + end + + protected + + def self.user_agent_list_key(time = Time.now.utc) + "crawl_ua_list:#{time.strftime('%Y%m%d')}" + end + + def self.redis_key(user_agent, time = Time.now.utc) + "crawl_req:#{time.strftime('%Y%m%d')}:#{user_agent}" + end + + def self.req_id(date, user_agent) + request_id(date: date, user_agent: user_agent) + end +end diff --git a/app/views/robots_txt/index.erb b/app/views/robots_txt/index.erb index abb3cf51fe..4f9bc5ac46 100644 --- a/app/views/robots_txt/index.erb +++ b/app/views/robots_txt/index.erb @@ -1,6 +1,8 @@ # See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file # -User-agent: * +<% @allowed_user_agents.each do |user_agent| %> +User-agent: <%= user_agent %> +<% end %> Disallow: /auth/cas Disallow: /auth/facebook/callback Disallow: /auth/twitter/callback @@ -29,4 +31,12 @@ Disallow: /groups Disallow: /groups/ Disallow: /uploads/ +<% if @disallowed_user_agents %> + <% @disallowed_user_agents.each do |user_agent| %> +User-agent: <%= user_agent %> +Disallow: / + + <% end %> +<% end %> + <%= server_plugin_outlet "robots_txt_index" %> diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 7e46d4b751..83f15ea40b 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -962,6 +962,10 @@ en: title: "User Visits" xaxis: "Day" yaxis: "Number of visits" + web_crawlers: + title: "Web Crawler Requests" + xaxis: "User Agent" + yaxis: "Pageviews" dashboard: rails_env_warning: "Your server is running in %{env} mode." @@ -1113,6 +1117,8 @@ en: blacklist_ip_blocks: "A list of private IP blocks that should never be crawled by Discourse" whitelist_internal_hosts: "A list of internal hosts that discourse can safely crawl for oneboxing and other purposes" allowed_iframes: "A list of iframe src domain prefixes that discourse can safely allow in posts" + whitelisted_crawler_user_agents: 'User agents of web crawlers that should be allowed to access the site.' + blacklisted_crawler_user_agents: 'User agents of web crawlers that should not be allowed to access the site. Does not apply if whitelist is defined.' 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/site_settings.yml b/config/site_settings.yml index dc00b023fe..3b1b7f00c2 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -1018,6 +1018,12 @@ security: default: 'https://www.google.com/maps/embed?|https://www.openstreetmap.org/export/embed.html?|https://calendar.google.com/calendar/embed?' type: list client: true + whitelisted_crawler_user_agents: + type: list + default: '' + blacklisted_crawler_user_agents: + type: list + default: '' onebox: enable_flash_video_onebox: false diff --git a/db/migrate/20180320190339_create_web_crawler_requests.rb b/db/migrate/20180320190339_create_web_crawler_requests.rb new file mode 100644 index 0000000000..f02fd77634 --- /dev/null +++ b/db/migrate/20180320190339_create_web_crawler_requests.rb @@ -0,0 +1,11 @@ +class CreateWebCrawlerRequests < ActiveRecord::Migration[5.1] + def change + create_table :web_crawler_requests do |t| + t.date :date, null: false + t.string :user_agent, null: false + t.integer :count, null: false, default: 0 + end + + add_index :web_crawler_requests, [:date, :user_agent], unique: true + end +end diff --git a/lib/crawler_detection.rb b/lib/crawler_detection.rb index 2958087a3c..bf28c6c1aa 100644 --- a/lib/crawler_detection.rb +++ b/lib/crawler_detection.rb @@ -28,4 +28,25 @@ module CrawlerDetection end end + + # Given a user_agent that returns true from crawler?, should its request be allowed? + def self.allow_crawler?(user_agent) + return true if SiteSetting.whitelisted_crawler_user_agents.blank? && + SiteSetting.blacklisted_crawler_user_agents.blank? + + @whitelisted_matchers ||= {} + @blacklisted_matchers ||= {} + + if SiteSetting.whitelisted_crawler_user_agents.present? + whitelisted = @whitelisted_matchers[SiteSetting.whitelisted_crawler_user_agents] ||= to_matcher(SiteSetting.whitelisted_crawler_user_agents) + !user_agent.nil? && user_agent.match?(whitelisted) + else + blacklisted = @blacklisted_matchers[SiteSetting.blacklisted_crawler_user_agents] ||= to_matcher(SiteSetting.blacklisted_crawler_user_agents) + user_agent.nil? || !user_agent.match?(blacklisted) + end + end + + def self.is_blocked_crawler?(user_agent) + crawler?(user_agent) && !allow_crawler?(user_agent) + end end diff --git a/lib/middleware/request_tracker.rb b/lib/middleware/request_tracker.rb index 2ad01b0521..aa23abd2ce 100644 --- a/lib/middleware/request_tracker.rb +++ b/lib/middleware/request_tracker.rb @@ -83,6 +83,7 @@ class Middleware::RequestTracker if track_view if data[:is_crawler] ApplicationRequest.increment!(:page_view_crawler) + WebCrawlerRequest.increment!(data[:user_agent]) elsif data[:has_auth_cookie] ApplicationRequest.increment!(:page_view_logged_in) ApplicationRequest.increment!(:page_view_logged_in_mobile) if data[:is_mobile] @@ -129,8 +130,9 @@ class Middleware::RequestTracker is_mobile: helper.is_mobile?, track_view: track_view, timing: timing - } - + }.tap do |h| + h[:user_agent] = env['HTTP_USER_AGENT'] if h[:is_crawler] + end end def log_request_info(env, result, info) @@ -155,12 +157,20 @@ class Middleware::RequestTracker def call(env) result = nil + log_request = true + request = Rack::Request.new(env) - if rate_limit(env) + if rate_limit(request) result = [429, {}, ["Slow down, too Many Requests from this IP Address"]] return result end + if block_crawler(request) + log_request = false + result = [403, {}, []] + return result + end + env["discourse.request_tracker"] = self MethodProfiler.start result = @app.call(env) @@ -186,7 +196,7 @@ class Middleware::RequestTracker end end end - log_request_info(env, result, info) unless env["discourse.request_tracker.skip"] + log_request_info(env, result, info) unless !log_request || env["discourse.request_tracker.skip"] end PRIVATE_IP ||= /^(127\.)|(192\.168\.)|(10\.)|(172\.1[6-9]\.)|(172\.2[0-9]\.)|(172\.3[0-1]\.)|(::1$)|([fF][cCdD])/ @@ -196,7 +206,7 @@ class Middleware::RequestTracker !!(ip && ip.to_s.match?(PRIVATE_IP)) end - def rate_limit(env) + def rate_limit(request) if ( GlobalSetting.max_reqs_per_ip_mode == "block" || @@ -204,7 +214,7 @@ class Middleware::RequestTracker GlobalSetting.max_reqs_per_ip_mode == "warn+block" ) - ip = Rack::Request.new(env).ip + ip = request.ip if !GlobalSetting.max_reqs_rate_limit_on_private return false if is_private_ip?(ip) @@ -236,15 +246,15 @@ class Middleware::RequestTracker global: true ) - env['DISCOURSE_RATE_LIMITERS'] = [limiter10, limiter60] - env['DISCOURSE_ASSET_RATE_LIMITERS'] = [limiter_assets10] + request.env['DISCOURSE_RATE_LIMITERS'] = [limiter10, limiter60] + request.env['DISCOURSE_ASSET_RATE_LIMITERS'] = [limiter_assets10] warn = GlobalSetting.max_reqs_per_ip_mode == "warn" || GlobalSetting.max_reqs_per_ip_mode == "warn+block" if !limiter_assets10.can_perform? if warn - Rails.logger.warn("Global asset IP rate limit exceeded for #{ip}: 10 second rate limit, uri: #{env["REQUEST_URI"]}") + Rails.logger.warn("Global asset IP rate limit exceeded for #{ip}: 10 second rate limit, uri: #{request.env["REQUEST_URI"]}") end return !(GlobalSetting.max_reqs_per_ip_mode == "warn") @@ -257,7 +267,7 @@ class Middleware::RequestTracker limiter60.performed! rescue RateLimiter::LimitExceeded if warn - Rails.logger.warn("Global IP rate limit exceeded for #{ip}: #{type} second rate limit, uri: #{env["REQUEST_URI"]}") + Rails.logger.warn("Global IP rate limit exceeded for #{ip}: #{type} second rate limit, uri: #{request.env["REQUEST_URI"]}") !(GlobalSetting.max_reqs_per_ip_mode == "warn") else true @@ -266,6 +276,11 @@ class Middleware::RequestTracker end end + def block_crawler(request) + !request.path.ends_with?('robots.txt') && + CrawlerDetection.is_blocked_crawler?(request.env['HTTP_USER_AGENT']) + end + def log_later(data, host) Scheduler::Defer.later("Track view", _db = nil) do self.class.log_request_on_site(data, host) diff --git a/spec/components/crawler_detection_spec.rb b/spec/components/crawler_detection_spec.rb index 86b5345042..65f811ef51 100644 --- a/spec/components/crawler_detection_spec.rb +++ b/spec/components/crawler_detection_spec.rb @@ -28,6 +28,12 @@ describe CrawlerDetection do expect(described_class.crawler?("Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)")).to eq(true) expect(described_class.crawler?("Baiduspider+(+http://www.baidu.com/search/spider.htm)")).to eq(true) expect(described_class.crawler?("Mozilla/5.0 (compatible; YandexBot/3.0; +http://yandex.com/bots)")).to eq(true) + + expect(described_class.crawler?("DiscourseAPI Ruby Gem 0.19.0")).to eq(true) + expect(described_class.crawler?("Pingdom.com_bot_version_1.4_(http://www.pingdom.com/)")).to eq(true) + expect(described_class.crawler?("LogicMonitor SiteMonitor/1.0")).to eq(true) + expect(described_class.crawler?("Java/1.8.0_151")).to eq(true) + expect(described_class.crawler?("Mozilla/5.0 (compatible; Yahoo! Slurp; http://help.yahoo.com/help/us/ysearch/slurp)")).to eq(true) end it "returns false for non-crawler user agents" do @@ -37,13 +43,106 @@ describe CrawlerDetection do expect(described_class.crawler?("Mozilla/5.0 (iPad; CPU OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5355d Safari/8536.25")).to eq(false) expect(described_class.crawler?("Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:25.0) Gecko/20100101 Firefox/25.0")).to eq(false) expect(described_class.crawler?("Mozilla/5.0 (Linux; U; Android 4.0.3; ko-kr; LG-L160L Build/IML74K) AppleWebkit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30")).to eq(false) - - expect(described_class.crawler?("DiscourseAPI Ruby Gem 0.19.0")).to eq(true) - expect(described_class.crawler?("Pingdom.com_bot_version_1.4_(http://www.pingdom.com/)")).to eq(true) - expect(described_class.crawler?("LogicMonitor SiteMonitor/1.0")).to eq(true) - expect(described_class.crawler?("Java/1.8.0_151")).to eq(true) - expect(described_class.crawler?("Mozilla/5.0 (compatible; Yahoo! Slurp; http://help.yahoo.com/help/us/ysearch/slurp)")).to eq(true) end end + + describe 'allow_crawler?' do + it 'returns true if whitelist and blacklist are blank' do + expect(CrawlerDetection.allow_crawler?('Googlebot/2.1 (+http://www.google.com/bot.html)')).to eq(true) + end + + context 'whitelist is set' do + before do + SiteSetting.whitelisted_crawler_user_agents = 'Googlebot|Twitterbot' + end + + it 'returns true for matching user agents' do + expect(CrawlerDetection.allow_crawler?('Googlebot/2.1 (+http://www.google.com/bot.html)')).to eq(true) + expect(CrawlerDetection.allow_crawler?('Googlebot-Image/1.0')).to eq(true) + expect(CrawlerDetection.allow_crawler?('Twitterbot')).to eq(true) + end + + it 'returns false for user agents that do not match' do + expect(CrawlerDetection.allow_crawler?('facebookexternalhit/1.1 (+http(s)://www.facebook.com/externalhit_uatext.php)')).to eq(false) + expect(CrawlerDetection.allow_crawler?('Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)')).to eq(false) + expect(CrawlerDetection.allow_crawler?('')).to eq(false) + end + + context 'and blacklist is set' do + before do + SiteSetting.blacklisted_crawler_user_agents = 'Googlebot-Image' + end + + it 'ignores the blacklist' do + expect(CrawlerDetection.allow_crawler?('Googlebot-Image/1.0')).to eq(true) + end + end + end + + context 'blacklist is set' do + before do + SiteSetting.blacklisted_crawler_user_agents = 'Googlebot|Twitterbot' + end + + it 'returns true for crawlers that do not match' do + expect(CrawlerDetection.allow_crawler?('Mediapartners-Google')).to eq(true) + expect(CrawlerDetection.allow_crawler?('facebookexternalhit/1.1 (+http(s)://www.facebook.com/externalhit_uatext.php)')).to eq(true) + expect(CrawlerDetection.allow_crawler?('')).to eq(true) + end + + it 'returns false for user agents that match' do + expect(CrawlerDetection.allow_crawler?('Googlebot/2.1 (+http://www.google.com/bot.html)')).to eq(false) + expect(CrawlerDetection.allow_crawler?('Googlebot-Image/1.0')).to eq(false) + expect(CrawlerDetection.allow_crawler?('Twitterbot')).to eq(false) + end + end + end + + describe 'is_blocked_crawler?' do + it 'is false if user agent is a crawler and no whitelist or blacklist is defined' do + expect(CrawlerDetection.is_blocked_crawler?('Twitterbot')).to eq(false) + end + + it 'is false if user agent is not a crawler and no whitelist or blacklist is defined' do + expect(CrawlerDetection.is_blocked_crawler?('Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36')).to eq(false) + end + + it 'is true if user agent is a crawler and is not whitelisted' do + SiteSetting.whitelisted_crawler_user_agents = 'Googlebot' + expect(CrawlerDetection.is_blocked_crawler?('Twitterbot')).to eq(true) + end + + it 'is false if user agent is not a crawler and there is a whitelist' do + SiteSetting.whitelisted_crawler_user_agents = 'Googlebot' + expect(CrawlerDetection.is_blocked_crawler?('Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36')).to eq(false) + end + + it 'is true if user agent is a crawler and is blacklisted' do + SiteSetting.blacklisted_crawler_user_agents = 'Twitterbot' + expect(CrawlerDetection.is_blocked_crawler?('Twitterbot')).to eq(true) + end + + it 'is true if user agent is a crawler and is not blacklisted' do + SiteSetting.blacklisted_crawler_user_agents = 'Twitterbot' + expect(CrawlerDetection.is_blocked_crawler?('Googlebot')).to eq(false) + end + + it 'is false if user agent is not a crawler and blacklist is defined' do + SiteSetting.blacklisted_crawler_user_agents = 'Mozilla' + expect(CrawlerDetection.is_blocked_crawler?('Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36')).to eq(false) + end + + it 'is true if user agent is missing and whitelist is defined' do + SiteSetting.whitelisted_crawler_user_agents = 'Googlebot' + expect(CrawlerDetection.is_blocked_crawler?('')).to eq(true) + expect(CrawlerDetection.is_blocked_crawler?(nil)).to eq(true) + end + + it 'is false if user agent is missing and blacklist is defined' do + SiteSetting.blacklisted_crawler_user_agents = 'Googlebot' + expect(CrawlerDetection.is_blocked_crawler?('')).to eq(false) + expect(CrawlerDetection.is_blocked_crawler?(nil)).to eq(false) + end + end end diff --git a/spec/components/middleware/request_tracker_spec.rb b/spec/components/middleware/request_tracker_spec.rb index bc7b1d608e..1838859ffb 100644 --- a/spec/components/middleware/request_tracker_spec.rb +++ b/spec/components/middleware/request_tracker_spec.rb @@ -273,4 +273,50 @@ describe Middleware::RequestTracker do expect(timing[:redis][:calls]).to eq 2 end end + + context "crawler blocking" do + let :middleware do + app = lambda do |env| + [200, {}, ['OK']] + end + + Middleware::RequestTracker.new(app) + end + + def expect_success_response(status, _, response) + expect(status).to eq(200) + expect(response).to eq(['OK']) + end + + def expect_blocked_response(status, _, response) + expect(status).to eq(403) + expect(response).to be_blank + end + + it "applies whitelisted_crawler_user_agents correctly" do + SiteSetting.whitelisted_crawler_user_agents = 'Googlebot' + expect_success_response(*middleware.call(env)) + expect_blocked_response(*middleware.call(env('HTTP_USER_AGENT' => 'Twitterbot'))) + expect_success_response(*middleware.call(env('HTTP_USER_AGENT' => 'Googlebot/2.1 (+http://www.google.com/bot.html)'))) + expect_blocked_response(*middleware.call(env('HTTP_USER_AGENT' => 'DiscourseAPI Ruby Gem 0.19.0'))) + end + + it "applies blacklisted_crawler_user_agents correctly" do + SiteSetting.blacklisted_crawler_user_agents = 'Googlebot' + expect_success_response(*middleware.call(env)) + expect_blocked_response(*middleware.call(env('HTTP_USER_AGENT' => 'Googlebot/2.1 (+http://www.google.com/bot.html)'))) + expect_success_response(*middleware.call(env('HTTP_USER_AGENT' => 'Twitterbot'))) + expect_success_response(*middleware.call(env('HTTP_USER_AGENT' => 'DiscourseAPI Ruby Gem 0.19.0'))) + end + + it "blocked crawlers shouldn't log page views" do + ApplicationRequest.clear_cache! + SiteSetting.blacklisted_crawler_user_agents = 'Googlebot' + expect { + middleware.call(env('HTTP_USER_AGENT' => 'Googlebot/2.1 (+http://www.google.com/bot.html)')) + ApplicationRequest.write_cache! + }.to_not change { ApplicationRequest.count } + end + end + end diff --git a/spec/fabricators/web_crawler_request_fabricator.rb b/spec/fabricators/web_crawler_request_fabricator.rb new file mode 100644 index 0000000000..ed678cc887 --- /dev/null +++ b/spec/fabricators/web_crawler_request_fabricator.rb @@ -0,0 +1,5 @@ +Fabricator(:web_crawler_request) do + user_agent { sequence(:ua) { |i| "Googlebot #{i}.0" } } + date Time.zone.now.to_date + count 0 +end diff --git a/spec/jobs/clean_up_crawler_stats_spec.rb b/spec/jobs/clean_up_crawler_stats_spec.rb new file mode 100644 index 0000000000..46c069e9e6 --- /dev/null +++ b/spec/jobs/clean_up_crawler_stats_spec.rb @@ -0,0 +1,31 @@ +require 'rails_helper' + +describe Jobs::CleanUpCrawlerStats do + subject { Jobs::CleanUpCrawlerStats.new.execute({}) } + + it "deletes records older than 30 days old" do + freeze_time + + today = Fabricate(:web_crawler_request, date: Time.zone.now.to_date) + yesterday = Fabricate(:web_crawler_request, date: 1.day.ago.to_date) + too_old = Fabricate(:web_crawler_request, date: 31.days.ago.to_date) + + expect { subject }.to change { WebCrawlerRequest.count }.by(-1) + expect(WebCrawlerRequest.where(id: too_old.id)).to_not exist + end + + it "keeps only the top records from the previous day" do + freeze_time + + WebCrawlerRequest.stubs(:max_records_per_day).returns(3) + + req1 = Fabricate(:web_crawler_request, date: 1.day.ago.to_date, count: 100) + req4 = Fabricate(:web_crawler_request, date: 1.day.ago.to_date, count: 30) + req3 = Fabricate(:web_crawler_request, date: 1.day.ago.to_date, count: 40) + req2 = Fabricate(:web_crawler_request, date: 1.day.ago.to_date, count: 50) + req5 = Fabricate(:web_crawler_request, date: 1.day.ago.to_date, count: 1) + + expect { subject }.to change { WebCrawlerRequest.count }.by(-2) + expect(WebCrawlerRequest.all).to contain_exactly(req1, req2, req3) + end +end diff --git a/spec/models/web_crawler_request_spec.rb b/spec/models/web_crawler_request_spec.rb new file mode 100644 index 0000000000..03f8da5a86 --- /dev/null +++ b/spec/models/web_crawler_request_spec.rb @@ -0,0 +1,114 @@ +require 'rails_helper' + +describe WebCrawlerRequest do + before do + WebCrawlerRequest.last_flush = Time.now.utc + WebCrawlerRequest.clear_cache! + end + + after do + WebCrawlerRequest.clear_cache! + end + + def inc(user_agent, opts = nil) + WebCrawlerRequest.increment!(user_agent, opts) + end + + def disable_date_flush! + freeze_time(Time.now) + WebCrawlerRequest.last_flush = Time.now.utc + end + + def web_crawler_request(user_agent) + WebCrawlerRequest.where(user_agent: user_agent).first + end + + it 'works even if redis is in readonly' do + disable_date_flush! + + inc('Googlebot') + inc('Googlebot') + + $redis.without_namespace.stubs(:incr).raises(Redis::CommandError.new("READONLY")) + $redis.without_namespace.stubs(:eval).raises(Redis::CommandError.new("READONLY")) + + inc('Googlebot', autoflush: 3) + WebCrawlerRequest.write_cache! + + $redis.without_namespace.unstub(:incr) + $redis.without_namespace.unstub(:eval) + + inc('Googlebot', autoflush: 3) + expect(web_crawler_request('Googlebot').count).to eq(3) + end + + it 'logs nothing for an unflushed increment' do + WebCrawlerRequest.increment!('Googlebot') + expect(WebCrawlerRequest.count).to eq(0) + end + + it 'can automatically flush' do + disable_date_flush! + + inc('Googlebot', autoflush: 3) + expect(web_crawler_request('Googlebot')).to_not be_present + expect(WebCrawlerRequest.count).to eq(0) + inc('Googlebot', autoflush: 3) + expect(web_crawler_request('Googlebot')).to_not be_present + inc('Googlebot', autoflush: 3) + expect(web_crawler_request('Googlebot').count).to eq(3) + expect(WebCrawlerRequest.count).to eq(1) + + 3.times { inc('Googlebot', autoflush: 3) } + expect(web_crawler_request('Googlebot').count).to eq(6) + expect(WebCrawlerRequest.count).to eq(1) + end + + it 'can flush based on time' do + t1 = Time.now.utc.at_midnight + freeze_time(t1) + WebCrawlerRequest.write_cache! + inc('Googlebot') + expect(WebCrawlerRequest.count).to eq(0) + + freeze_time(t1 + WebCrawlerRequest.autoflush_seconds + 1) + inc('Googlebot') + + expect(WebCrawlerRequest.count).to eq(1) + end + + it 'flushes yesterdays results' do + t1 = Time.now.utc.at_midnight + freeze_time(t1) + inc('Googlebot') + freeze_time(t1.tomorrow) + inc('Googlebot') + + WebCrawlerRequest.write_cache! + expect(WebCrawlerRequest.count).to eq(2) + end + + it 'clears cache correctly' do + inc('Googlebot') + inc('Twitterbot') + WebCrawlerRequest.clear_cache! + WebCrawlerRequest.write_cache! + + expect(WebCrawlerRequest.count).to eq(0) + end + + it 'logs a few counts once flushed' do + time = Time.now.at_midnight + freeze_time(time) + + 3.times { inc('Googlebot') } + 2.times { inc('Twitterbot') } + 4.times { inc('Bingbot') } + + WebCrawlerRequest.write_cache! + + expect(web_crawler_request('Googlebot').count).to eq(3) + expect(web_crawler_request('Twitterbot').count).to eq(2) + expect(web_crawler_request('Bingbot').count).to eq(4) + end +end diff --git a/spec/requests/robots_txt_controller_spec.rb b/spec/requests/robots_txt_controller_spec.rb index 68d5912be4..be3590e8d8 100644 --- a/spec/requests/robots_txt_controller_spec.rb +++ b/spec/requests/robots_txt_controller_spec.rb @@ -2,11 +2,64 @@ require 'rails_helper' RSpec.describe RobotsTxtController do describe '#index' do - it "returns index when indexing is allowed" do - SiteSetting.allow_index_in_robots_txt = true - get '/robots.txt' + context 'allow_index_in_robots_txt is true' do - expect(response.body).to include("Disallow: /u/") + def expect_allowed_and_disallowed_sections(allow_index, disallow_index) + expect(allow_index).to be_present + expect(disallow_index).to be_present + + allow_section = allow_index < disallow_index ? + response.body[allow_index...disallow_index] : response.body[allow_index..-1] + + expect(allow_section).to include('Disallow: /u/') + expect(allow_section).to_not include("Disallow: /\n") + + disallowed_section = allow_index < disallow_index ? + response.body[disallow_index..-1] : response.body[disallow_index...allow_index] + expect(disallowed_section).to include("Disallow: /\n") + end + + it "returns index when indexing is allowed" do + SiteSetting.allow_index_in_robots_txt = true + get '/robots.txt' + + i = response.body.index('User-agent: *') + expect(i).to be_present + expect(response.body[i..-1]).to include("Disallow: /u/") + end + + it "can whitelist user agents" do + SiteSetting.whitelisted_crawler_user_agents = "Googlebot|Twitterbot" + get '/robots.txt' + expect(response.body).to include('User-agent: Googlebot') + expect(response.body).to include('User-agent: Twitterbot') + + allowed_index = [response.body.index('User-agent: Googlebot'), response.body.index('User-agent: Twitterbot')].min + disallow_all_index = response.body.index('User-agent: *') + + expect_allowed_and_disallowed_sections(allowed_index, disallow_all_index) + end + + it "can blacklist user agents" do + SiteSetting.blacklisted_crawler_user_agents = "Googlebot|Twitterbot" + get '/robots.txt' + expect(response.body).to include('User-agent: Googlebot') + expect(response.body).to include('User-agent: Twitterbot') + + disallow_index = [response.body.index('User-agent: Googlebot'), response.body.index('User-agent: Twitterbot')].min + allow_index = response.body.index('User-agent: *') + + expect_allowed_and_disallowed_sections(allow_index, disallow_index) + end + + it "ignores blacklist if whitelist is set" do + SiteSetting.whitelisted_crawler_user_agents = "Googlebot|Twitterbot" + SiteSetting.blacklisted_crawler_user_agents = "Bananabot" + get '/robots.txt' + expect(response.body).to_not include('Bananabot') + expect(response.body).to include('User-agent: Googlebot') + expect(response.body).to include('User-agent: Twitterbot') + end end it "returns noindex when indexing is disallowed" do From a84bb81ab52ccf107f15a37c1bd540b8d63a565b Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Thu, 22 Mar 2018 17:57:44 -0400 Subject: [PATCH 003/287] only applies to get html requests --- lib/middleware/request_tracker.rb | 5 ++++- spec/components/middleware/request_tracker_spec.rb | 9 +++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/middleware/request_tracker.rb b/lib/middleware/request_tracker.rb index aa23abd2ce..d22fbfb06d 100644 --- a/lib/middleware/request_tracker.rb +++ b/lib/middleware/request_tracker.rb @@ -277,7 +277,10 @@ class Middleware::RequestTracker end def block_crawler(request) - !request.path.ends_with?('robots.txt') && + request.get? && + !request.xhr? && + request.env['HTTP_ACCEPT'] =~ /text\/html/ && + !request.path.ends_with?('robots.txt') && CrawlerDetection.is_blocked_crawler?(request.env['HTTP_USER_AGENT']) end diff --git a/spec/components/middleware/request_tracker_spec.rb b/spec/components/middleware/request_tracker_spec.rb index 1838859ffb..57f5782121 100644 --- a/spec/components/middleware/request_tracker_spec.rb +++ b/spec/components/middleware/request_tracker_spec.rb @@ -9,6 +9,7 @@ describe Middleware::RequestTracker do "HTTP_USER_AGENT" => "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36", "REQUEST_URI" => "/path?bla=1", "REQUEST_METHOD" => "GET", + "HTTP_ACCEPT" => "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", "rack.input" => "" }.merge(opts) end @@ -317,6 +318,14 @@ describe Middleware::RequestTracker do ApplicationRequest.write_cache! }.to_not change { ApplicationRequest.count } end + + it "allows json requests" do + SiteSetting.blacklisted_crawler_user_agents = 'Googlebot' + expect_success_response(*middleware.call(env( + 'HTTP_USER_AGENT' => 'Googlebot/2.1 (+http://www.google.com/bot.html)', + 'HTTP_ACCEPT' => 'application/json' + ))) + end end end From fa0868fc3f7a493dc5caf75ce6b66960cf33d6cb Mon Sep 17 00:00:00 2001 From: Kevin Elliott Date: Fri, 23 Mar 2018 09:59:31 -0700 Subject: [PATCH 004/287] Explicit param permit and assignment cleanup. --- app/controllers/topics_controller.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index 9f6467212d..db5ab9ae35 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -668,9 +668,10 @@ class TopicsController < ApplicationController raise ActionController::ParameterMissing.new(:topic_ids) end - operation = params.require(:operation) - operation.permit! - operation = operation.to_h.symbolize_keys + operation = params + .require(:operation) + .permit(:type, :group, :category_id, :notification_level_id, :tags) + .to_h.symbolize_keys raise ActionController::ParameterMissing.new(:operation_type) if operation[:type].blank? operator = TopicsBulkAction.new(current_user, topic_ids, operation, group: operation[:group]) From 84e1ffd141086e39fa2d379e426379421633c9a1 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 26 Mar 2018 12:48:28 -0400 Subject: [PATCH 005/287] Update rails-html-sanitizer --- Gemfile.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 4ee395b285..eda3435e7c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -159,7 +159,7 @@ GEM logstash-logger (0.25.1) logstash-event (~> 1.2) logster (1.2.9) - loofah (2.2.1) + loofah (2.2.2) crass (~> 1.0.2) nokogiri (>= 1.5.9) lru_redux (1.1.0) @@ -269,8 +269,8 @@ GEM rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) - rails-html-sanitizer (1.0.3) - loofah (~> 2.0) + rails-html-sanitizer (1.0.4) + loofah (~> 2.2, >= 2.2.2) rails_multisite (2.0.4) activerecord (> 4.2, < 6) railties (> 4.2, < 6) From 2ca37602d94687e16a54be9fbb0587832a87d292 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 26 Mar 2018 12:49:54 -0400 Subject: [PATCH 006/287] Update rack-protection --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index eda3435e7c..f3b04f834b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -256,13 +256,13 @@ GEM public_suffix (2.0.5) puma (3.9.1) r2 (0.2.6) - rack (2.0.3) + rack (2.0.4) rack-mini-profiler (0.10.7) rack (>= 1.2.0) rack-openid (1.3.1) rack (>= 1.1.0) ruby-openid (>= 2.1.8) - rack-protection (2.0.0) + rack-protection (2.0.1) rack rack-test (0.7.0) rack (>= 1.0, < 3) From 09b9b56091ee029f45f7f79f30e2b2fa05470de2 Mon Sep 17 00:00:00 2001 From: Kris Date: Mon, 26 Mar 2018 14:04:42 -0400 Subject: [PATCH 007/287] adding a class to post activity link --- .../discourse/templates/list/activity-column.raw.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/templates/list/activity-column.raw.hbs b/app/assets/javascripts/discourse/templates/list/activity-column.raw.hbs index 56a994a551..5bb3d82280 100644 --- a/app/assets/javascripts/discourse/templates/list/activity-column.raw.hbs +++ b/app/assets/javascripts/discourse/templates/list/activity-column.raw.hbs @@ -1 +1 @@ -<{{tagName}} class="{{class}} {{cold-age-class topic.createdAt startDate=topic.bumpedAt class=""}} activity" title="{{{topic.bumpedAtTitle}}}">{{format-date topic.bumpedAt format="tiny" noTitle="true"}} +<{{tagName}} class="{{class}} {{cold-age-class topic.createdAt startDate=topic.bumpedAt class=""}} activity" title="{{{topic.bumpedAtTitle}}}">{{format-date topic.bumpedAt format="tiny" noTitle="true"}} From d4296f33fffa6d61d44aa948900e4fca9baa19d3 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 26 Mar 2018 15:46:04 -0400 Subject: [PATCH 008/287] FIX: Publishing should update the public_version too --- lib/topic_publisher.rb | 1 + spec/components/topic_publisher_spec.rb | 1 + 2 files changed, 2 insertions(+) diff --git a/lib/topic_publisher.rb b/lib/topic_publisher.rb index 3a901ad1c9..5003f70cb5 100644 --- a/lib/topic_publisher.rb +++ b/lib/topic_publisher.rb @@ -30,6 +30,7 @@ class TopicPublisher if op.present? op.revisions.delete_all op.update_column(:version, 1) + op.update_column(:public_version, 1) end end diff --git a/spec/components/topic_publisher_spec.rb b/spec/components/topic_publisher_spec.rb index 7f4dd740b1..aed3bce232 100644 --- a/spec/components/topic_publisher_spec.rb +++ b/spec/components/topic_publisher_spec.rb @@ -39,6 +39,7 @@ describe TopicPublisher do # Should delete any edits on the OP expect(op.revisions.size).to eq(0) expect(op.version).to eq(1) + expect(op.public_version).to eq(1) end end From f03b6bd8c925163b41b9e33b39484d102dbaee44 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 26 Mar 2018 16:06:20 -0400 Subject: [PATCH 009/287] FIX: Update `last_version_at` when publishing --- lib/topic_publisher.rb | 10 ++++--- spec/components/topic_publisher_spec.rb | 36 +++++++++++++++---------- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/lib/topic_publisher.rb b/lib/topic_publisher.rb index 5003f70cb5..a7cecb7e4f 100644 --- a/lib/topic_publisher.rb +++ b/lib/topic_publisher.rb @@ -7,7 +7,8 @@ class TopicPublisher end def publish! - TopicTimestampChanger.new(timestamp: Time.zone.now, topic: @topic).change! do + published_at = Time.zone.now + TopicTimestampChanger.new(timestamp: published_at, topic: @topic).change! do if @topic.private_message? @topic = TopicConverter.new(@topic, @published_by) .convert_to_public_topic(@category_id) @@ -29,8 +30,11 @@ class TopicPublisher op = @topic.first_post if op.present? op.revisions.delete_all - op.update_column(:version, 1) - op.update_column(:public_version, 1) + op.update_columns( + version: 1, + public_version: 1, + last_version_at: published_at + ) end end diff --git a/spec/components/topic_publisher_spec.rb b/spec/components/topic_publisher_spec.rb index aed3bce232..43d33172f5 100644 --- a/spec/components/topic_publisher_spec.rb +++ b/spec/components/topic_publisher_spec.rb @@ -24,22 +24,30 @@ describe TopicPublisher do end it "will publish the topic properly" do - TopicPublisher.new(topic, moderator, shared_draft.category_id).publish! + published_at = 1.hour.from_now.change(usec: 0) + freeze_time(published_at) do + TopicPublisher.new(topic, moderator, shared_draft.category_id).publish! - topic.reload - expect(topic.category).to eq(category) - expect(topic).to be_visible - expect(topic.shared_draft).to be_blank - expect(UserHistory.where( - acting_user_id: moderator.id, - action: UserHistory.actions[:topic_published] - )).to be_present - op.reload + topic.reload + expect(topic.category).to eq(category) + expect(topic).to be_visible + expect(topic.created_at).to eq(published_at) + expect(topic.updated_at).to eq(published_at) + expect(topic.shared_draft).to be_blank + expect(UserHistory.where( + acting_user_id: moderator.id, + action: UserHistory.actions[:topic_published] + )).to be_present + op.reload - # Should delete any edits on the OP - expect(op.revisions.size).to eq(0) - expect(op.version).to eq(1) - expect(op.public_version).to eq(1) + # Should delete any edits on the OP + expect(op.revisions.size).to eq(0) + expect(op.version).to eq(1) + expect(op.public_version).to eq(1) + expect(op.created_at).to eq(published_at) + expect(op.updated_at).to eq(published_at) + expect(op.last_version_at).to eq(published_at) + end end end From f2c060bdf2a309fd4ba884b529367df66837a6d7 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Mon, 26 Mar 2018 17:04:55 -0400 Subject: [PATCH 010/287] FEATURE: option for tags in a tag group to be visible only to staff --- .../discourse/models/tag-group.js.es6 | 3 +- .../discourse/templates/tag-groups-show.hbs | 7 +++ app/controllers/tag_groups_controller.rb | 15 +++++- app/controllers/tags_controller.rb | 5 +- app/models/category.rb | 51 +++++++++--------- app/models/group.rb | 9 ++++ app/models/tag.rb | 3 ++ app/models/tag_group.rb | 43 ++++++++++++++- app/models/tag_group_permission.rb | 9 ++++ app/serializers/concerns/topic_tags_mixin.rb | 2 +- app/serializers/post_revision_serializer.rb | 11 +++- app/serializers/tag_group_serializer.rb | 2 +- config/locales/client.en.yml | 1 + ...0323154826_create_tag_group_permissions.rb | 10 ++++ lib/discourse_tagging.rb | 32 ++++++++++- spec/components/discourse_tagging_spec.rb | 44 +++++++++++++-- .../discourse_tagging_spec.rb.rb} | 0 spec/models/tag_spec.rb | 18 +++++++ .../post_revision_serializer_spec.rb | 53 +++++++++++++++++++ .../suggested_topic_serializer_spec.rb | 25 ++++++++- .../topic_list_item_serializer_spec.rb | 21 ++++++++ .../serializers/topic_view_serializer_spec.rb | 29 ++++++++-- 22 files changed, 347 insertions(+), 46 deletions(-) create mode 100644 app/models/tag_group_permission.rb create mode 100644 db/migrate/20180323154826_create_tag_group_permissions.rb rename spec/{discourse_tagging_spec.rb => components/discourse_tagging_spec.rb.rb} (100%) create mode 100644 spec/serializers/post_revision_serializer_spec.rb diff --git a/app/assets/javascripts/discourse/models/tag-group.js.es6 b/app/assets/javascripts/discourse/models/tag-group.js.es6 index cfbd83e164..f433038abb 100644 --- a/app/assets/javascripts/discourse/models/tag-group.js.es6 +++ b/app/assets/javascripts/discourse/models/tag-group.js.es6 @@ -24,7 +24,8 @@ const TagGroup = RestModel.extend({ name: this.get('name'), tag_names: this.get('tag_names'), parent_tag_name: this.get('parent_tag_name') ? this.get('parent_tag_name') : undefined, - one_per_topic: this.get('one_per_topic') + one_per_topic: this.get('one_per_topic'), + permissions: this.get('visible_only_to_staff') ? {"staff": "1"} : {"everyone": "1"} }, type: isNew ? 'POST' : 'PUT' }).then(function(result) { diff --git a/app/assets/javascripts/discourse/templates/tag-groups-show.hbs b/app/assets/javascripts/discourse/templates/tag-groups-show.hbs index 0a7dc1ef70..032f0ce06e 100644 --- a/app/assets/javascripts/discourse/templates/tag-groups-show.hbs +++ b/app/assets/javascripts/discourse/templates/tag-groups-show.hbs @@ -28,6 +28,13 @@ +
+ +
+ {{model.savingStatus}} diff --git a/app/controllers/tag_groups_controller.rb b/app/controllers/tag_groups_controller.rb index 8d819827f7..069f73830a 100644 --- a/app/controllers/tag_groups_controller.rb +++ b/app/controllers/tag_groups_controller.rb @@ -70,7 +70,20 @@ class TagGroupsController < ApplicationController end def tag_groups_params - result = params.permit(:id, :name, :one_per_topic, tag_names: [], parent_tag_name: []) + if permissions = params[:permissions] + permissions.each do |k, v| + permissions[k] = v.to_i + end + end + + result = params.permit( + :id, + :name, + :one_per_topic, + tag_names: [], + parent_tag_name: [], + permissions: [*permissions&.keys] + ) result[:tag_names] ||= [] result[:parent_tag_name] ||= [] result[:one_per_topic] = (params[:one_per_topic] == "true") diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index 3a6d5f0d67..5c4e798b99 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -44,7 +44,10 @@ class TagsController < ::ApplicationController extras: { tag_groups: grouped_tag_counts } } else - unrestricted_tags = Tag.where("tags.id NOT IN (select tag_id from category_tags) AND tags.topic_count > 0") + unrestricted_tags = DiscourseTagging.filter_visible( + Tag.where("tags.id NOT IN (select tag_id from category_tags) AND tags.topic_count > 0"), + guardian + ) categories = Category.where("id in (select category_id from category_tags)") .where("id in (?)", guardian.allowed_category_ids) diff --git a/app/models/category.rb b/app/models/category.rb index e551227ed6..b2b7242873 100644 --- a/app/models/category.rb +++ b/app/models/category.rb @@ -321,6 +321,30 @@ SQL end end + def self.resolve_permissions(permissions) + read_restricted = true + + everyone = Group::AUTO_GROUPS[:everyone] + full = CategoryGroup.permission_types[:full] + + mapped = permissions.map do |group, permission| + group_id = Group.group_id_from_param(group) + permission = CategoryGroup.permission_types[permission] unless permission.is_a?(Integer) + + [group_id, permission] + end + + mapped.each do |group, permission| + if group == everyone && permission == full + return [false, []] + end + + read_restricted = false if group == everyone + end + + [read_restricted, mapped] + end + def allowed_tags=(tag_names_arg) DiscourseTagging.add_or_create_tags_by_name(self, tag_names_arg, unlimited: true) end @@ -379,33 +403,6 @@ SQL self.update_attributes(latest_topic_id: latest_topic_id, latest_post_id: latest_post_id) end - def self.resolve_permissions(permissions) - read_restricted = true - - everyone = Group::AUTO_GROUPS[:everyone] - full = CategoryGroup.permission_types[:full] - - mapped = permissions.map do |group, permission| - group = group.id if group.is_a?(Group) - - # subtle, using Group[] ensures the group exists in the DB - group = Group[group.to_sym].id unless group.is_a?(Integer) - permission = CategoryGroup.permission_types[permission] unless permission.is_a?(Integer) - - [group, permission] - end - - mapped.each do |group, permission| - if group == everyone && permission == full - return [false, []] - end - - read_restricted = false if group == everyone - end - - [read_restricted, mapped] - end - def self.query_parent_category(parent_slug) self.where(slug: parent_slug, parent_category_id: nil).pluck(:id).first || self.where(id: parent_slug.to_i).pluck(:id).first diff --git a/app/models/group.rb b/app/models/group.rb index 90bb845f63..5d8e84436e 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -434,6 +434,15 @@ class Group < ActiveRecord::Base end end + # given something that might be a group name, id, or record, return the group id + def self.group_id_from_param(group_param) + return group_param.id if group_param.is_a?(Group) + return group_param if group_param.is_a?(Integer) + + # subtle, using Group[] ensures the group exists in the DB + Group[group_param.to_sym].id + end + def self.builtin Enum.new(:moderators, :admins, :trust_level_1, :trust_level_2) end diff --git a/app/models/tag.rb b/app/models/tag.rb index f110e677ce..d452c66559 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -46,11 +46,14 @@ class Tag < ActiveRecord::Base return [] if scope_category_ids.empty? + filter_sql = guardian&.is_staff? ? '' : (' AND ' + DiscourseTagging.filter_visible_sql) + tag_names_with_counts = Tag.exec_sql <<~SQL SELECT tags.name as tag_name, SUM(stats.topic_count) AS sum_topic_count FROM category_tag_stats stats INNER JOIN tags ON stats.tag_id = tags.id AND stats.topic_count > 0 WHERE stats.category_id in (#{scope_category_ids.join(',')}) + #{filter_sql} GROUP BY tags.name ORDER BY sum_topic_count DESC, tag_name ASC LIMIT #{limit} diff --git a/app/models/tag_group.rb b/app/models/tag_group.rb index b1c7088c70..2ed60e0780 100644 --- a/app/models/tag_group.rb +++ b/app/models/tag_group.rb @@ -5,9 +5,14 @@ class TagGroup < ActiveRecord::Base has_many :tags, through: :tag_group_memberships has_many :category_tag_groups, dependent: :destroy has_many :categories, through: :category_tag_groups + has_many :tag_group_permissions, dependent: :destroy belongs_to :parent_tag, class_name: 'Tag' + before_save :apply_permissions + + attr_accessor :permissions + def tag_names=(tag_names_arg) DiscourseTagging.add_or_create_tags_by_name(self, tag_names_arg, unlimited: true) end @@ -22,13 +27,47 @@ class TagGroup < ActiveRecord::Base end end + def permissions=(permissions) + @permissions = TagGroup.resolve_permissions(permissions) + end + + def self.resolve_permissions(permissions) + everyone_group_id = Group::AUTO_GROUPS[:everyone] + full = TagGroupPermission.permission_types[:full] + + mapped = permissions.map do |group, permission| + group_id = Group.group_id_from_param(group) + permission = TagGroupPermission.permission_types[permission] unless permission.is_a?(Integer) + + return [] if group_id == everyone_group_id && permission == full + + [group_id, permission] + end + end + + def apply_permissions + if @permissions + tag_group_permissions.destroy_all + @permissions.each do |group_id, permission_type| + tag_group_permissions.build(group_id: group_id, permission_type: permission_type) + end + @permissions = nil + end + end + + def visible_only_to_staff + # currently only "everyone" and "staff" groups are supported + tag_group_permissions.count > 0 + end + def self.allowed(guardian) if guardian.is_staff? TagGroup else category_permissions_filter = <<~SQL - id IN ( SELECT tag_group_id FROM category_tag_groups WHERE category_id IN (?)) - OR id NOT IN (SELECT tag_group_id FROM category_tag_groups) + (id IN ( SELECT tag_group_id FROM category_tag_groups WHERE category_id IN (?)) + OR id NOT IN (SELECT tag_group_id FROM category_tag_groups)) + AND id NOT IN (SELECT tag_group_id FROM tag_group_permissions) SQL TagGroup.where(category_permissions_filter, guardian.allowed_category_ids) diff --git a/app/models/tag_group_permission.rb b/app/models/tag_group_permission.rb new file mode 100644 index 0000000000..2d77af228f --- /dev/null +++ b/app/models/tag_group_permission.rb @@ -0,0 +1,9 @@ +# Who can see and use tags belonging to a tag group. +class TagGroupPermission < ActiveRecord::Base + belongs_to :tag_group + belongs_to :group + + def self.permission_types + @permission_types ||= Enum.new(full: 1) #, see: 2 + end +end diff --git a/app/serializers/concerns/topic_tags_mixin.rb b/app/serializers/concerns/topic_tags_mixin.rb index 6ad88b47ea..c2dc1254d2 100644 --- a/app/serializers/concerns/topic_tags_mixin.rb +++ b/app/serializers/concerns/topic_tags_mixin.rb @@ -9,7 +9,7 @@ module TopicTagsMixin def tags # Calling method `pluck` along with `includes` causing N+1 queries - topic.tags.map(&:name) + DiscourseTagging.filter_visible(topic.tags, scope).map(&:name) end def topic diff --git a/app/serializers/post_revision_serializer.rb b/app/serializers/post_revision_serializer.rb index b86fda2379..0e0348cb39 100644 --- a/app/serializers/post_revision_serializer.rb +++ b/app/serializers/post_revision_serializer.rb @@ -160,7 +160,11 @@ class PostRevisionSerializer < ApplicationSerializer end def tags_changes - { previous: previous["tags"], current: current["tags"] } + changes = { + previous: filter_visible_tags(previous["tags"]), + current: filter_visible_tags(current["tags"]) + } + changes[:previous] == changes[:current] ? nil : changes end def include_tags_changes? @@ -250,4 +254,9 @@ class PostRevisionSerializer < ApplicationSerializer object.user || Discourse.system_user end + def filter_visible_tags(tags) + @hidden_tag_names ||= DiscourseTagging.hidden_tag_names(scope) + tags.is_a?(Array) ? (tags - @hidden_tag_names) : tags + end + end diff --git a/app/serializers/tag_group_serializer.rb b/app/serializers/tag_group_serializer.rb index 62858aff20..e0a4734f24 100644 --- a/app/serializers/tag_group_serializer.rb +++ b/app/serializers/tag_group_serializer.rb @@ -1,5 +1,5 @@ class TagGroupSerializer < ApplicationSerializer - attributes :id, :name, :tag_names, :parent_tag_name, :one_per_topic + attributes :id, :name, :tag_names, :parent_tag_name, :one_per_topic, :visible_only_to_staff def tag_names object.tags.map(&:name).sort diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 523163cd46..d3144ca4c2 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -2662,6 +2662,7 @@ en: save: "Save" delete: "Delete" confirm_delete: "Are you sure you want to delete this tag group?" + visible_only_to_staff: "Tags are visible only to staff" topics: none: diff --git a/db/migrate/20180323154826_create_tag_group_permissions.rb b/db/migrate/20180323154826_create_tag_group_permissions.rb new file mode 100644 index 0000000000..74abffefc8 --- /dev/null +++ b/db/migrate/20180323154826_create_tag_group_permissions.rb @@ -0,0 +1,10 @@ +class CreateTagGroupPermissions < ActiveRecord::Migration[5.1] + def change + create_table :tag_group_permissions do |t| + t.references :tag_group, null: false + t.references :group, null: false + t.integer :permission_type, default: 1, null: false + t.timestamps null: false + end + end +end diff --git a/lib/discourse_tagging.rb b/lib/discourse_tagging.rb index 758b8dcd75..2ae28445d2 100644 --- a/lib/discourse_tagging.rb +++ b/lib/discourse_tagging.rb @@ -76,7 +76,6 @@ module DiscourseTagging term.gsub!("_", "\\_") term = clean_tag(term) query = query.where('tags.name like ?', "%#{term}%") - # query = query.where('tags.id NOT IN (?)', selected_tag_ids) unless selected_tag_ids.empty? end # Filters for category-specific tags: @@ -152,7 +151,36 @@ module DiscourseTagging end end - query + if guardian.nil? || guardian.is_staff? + query + else + filter_visible(query, guardian) + end + end + + def self.filter_visible(query, guardian = nil) + if !guardian&.is_staff? && TagGroupPermission.where(group_id: Group::AUTO_GROUPS[:staff]).exists? + query.where(filter_visible_sql) + else + query + end + end + + def self.filter_visible_sql + @filter_visible_sql ||= <<~SQL + tags.id NOT IN ( + SELECT tgm.tag_id + FROM tag_group_memberships tgm + INNER JOIN tag_group_permissions tgp + ON tgp.tag_group_id = tgm.tag_group_id + AND tgp.group_id = #{Group::AUTO_GROUPS[:staff]}) + SQL + end + + def self.hidden_tag_names(guardian = nil) + return [] if guardian&.is_staff? || !TagGroupPermission.where(group_id: Group::AUTO_GROUPS[:staff]).exists? + tag_group_ids = TagGroupPermission.where(group_id: Group::AUTO_GROUPS[:staff]).pluck(:tag_group_id) + Tag.includes(:tag_groups).where('tag_group_id in (?)', tag_group_ids).pluck(:name) end def self.clean_tag(tag) diff --git a/spec/components/discourse_tagging_spec.rb b/spec/components/discourse_tagging_spec.rb index ab15f0b0d2..adc6c05a87 100644 --- a/spec/components/discourse_tagging_spec.rb +++ b/spec/components/discourse_tagging_spec.rb @@ -7,11 +7,12 @@ require 'discourse_tagging' describe DiscourseTagging do + let(:admin) { Fabricate(:admin) } let(:user) { Fabricate(:user) } - let!(:tag1) { Fabricate(:tag, name: "tag1") } - let!(:tag2) { Fabricate(:tag, name: "tag2") } - let!(:tag3) { Fabricate(:tag, name: "tag3") } + let!(:tag1) { Fabricate(:tag, name: "fun") } + let!(:tag2) { Fabricate(:tag, name: "fun2") } + let!(:tag3) { Fabricate(:tag, name: "fun3") } before do SiteSetting.tagging_enabled = true @@ -25,7 +26,7 @@ describe DiscourseTagging do tags = DiscourseTagging.filter_allowed_tags(Tag.all, Guardian.new(user), selected_tags: [tag2.name], for_input: true, - term: 'tag' + term: 'fun' ).map(&:name) expect(tags).to contain_exactly(tag1.name, tag3.name) end @@ -37,6 +38,41 @@ describe DiscourseTagging do ).map(&:name) expect(tags).to contain_exactly(tag1.name, tag3.name) end + + context 'with tags visible only to staff' do + let(:hidden_tag) { Fabricate(:tag) } + let!(:staff_tag_group) { Fabricate(:tag_group, permissions: { "staff" => 1 }, tag_names: [hidden_tag.name]) } + + it 'should return all tags to staff' do + tags = DiscourseTagging.filter_allowed_tags(Tag.all, Guardian.new(admin)).to_a + expect(tags).to contain_exactly(tag1, tag2, tag3, hidden_tag) + expect(tags.size).to eq(4) + end + + it 'should not return hidden tag to non-staff' do + tags = DiscourseTagging.filter_allowed_tags(Tag.all, Guardian.new(user)).to_a + expect(tags).to contain_exactly(tag1, tag2, tag3) + expect(tags.size).to eq(3) + end + end + end + end + + describe 'filter_visible' do + let(:hidden_tag) { Fabricate(:tag) } + let!(:staff_tag_group) { Fabricate(:tag_group, permissions: { "staff" => 1 }, tag_names: [hidden_tag.name]) } + let(:topic) { Fabricate(:topic, tags: [tag1, tag2, tag3, hidden_tag]) } + + it 'returns all tags to staff' do + tags = DiscourseTagging.filter_visible(topic.tags, Guardian.new(admin)) + expect(tags.size).to eq(4) + expect(tags).to contain_exactly(tag1, tag2, tag3, hidden_tag) + end + + it 'does not return hidden tags to non-staff' do + tags = DiscourseTagging.filter_visible(topic.tags, Guardian.new(user)) + expect(tags.size).to eq(3) + expect(tags).to contain_exactly(tag1, tag2, tag3) end end end diff --git a/spec/discourse_tagging_spec.rb b/spec/components/discourse_tagging_spec.rb.rb similarity index 100% rename from spec/discourse_tagging_spec.rb rename to spec/components/discourse_tagging_spec.rb.rb diff --git a/spec/models/tag_spec.rb b/spec/models/tag_spec.rb index 1d792c10a2..d915429fab 100644 --- a/spec/models/tag_spec.rb +++ b/spec/models/tag_spec.rb @@ -94,6 +94,24 @@ describe Tag do expect(described_class.top_tags.sort).to eq([@tags[0].name, @tags[1].name, @tags[2].name].sort) end end + + context "with hidden tags" do + let(:hidden_tag) { Fabricate(:tag, name: 'hidden') } + let!(:staff_tag_group) { Fabricate(:tag_group, permissions: { "staff" => 1 }, tag_names: [hidden_tag.name]) } + let!(:topic2) { Fabricate(:topic, tags: [tag, hidden_tag]) } + + it "returns all tags to staff" do + expect(Tag.top_tags(guardian: Guardian.new(Fabricate(:admin)))).to include(hidden_tag.name) + end + + it "doesn't return hidden tags to anon" do + expect(Tag.top_tags).to_not include(hidden_tag.name) + end + + it "doesn't return hidden tags to non-staff" do + expect(Tag.top_tags(guardian: Guardian.new(Fabricate(:user)))).to_not include(hidden_tag.name) + end + end end describe '#pm_tags' do diff --git a/spec/serializers/post_revision_serializer_spec.rb b/spec/serializers/post_revision_serializer_spec.rb new file mode 100644 index 0000000000..9bed7723e0 --- /dev/null +++ b/spec/serializers/post_revision_serializer_spec.rb @@ -0,0 +1,53 @@ +require 'rails_helper' + +describe PostRevisionSerializer do + let(:post) { Fabricate(:post, version: 2) } + + context 'hidden tags' do + let(:public_tag) { Fabricate(:tag, name: 'public') } + let(:public_tag2) { Fabricate(:tag, name: 'visible') } + let(:hidden_tag) { Fabricate(:tag, name: 'hidden') } + let(:hidden_tag2) { Fabricate(:tag, name: 'secret') } + + let(:staff_tag_group) { Fabricate(:tag_group, permissions: { "staff" => 1 }, tag_names: [hidden_tag.name, hidden_tag2.name]) } + + let(:post_revision) do + Fabricate(:post_revision, + post: post, + modifications: { 'tags' => [['public', 'hidden'], ['visible', 'hidden']] } + ) + end + + let(:post_revision2) do + Fabricate(:post_revision, + post: post, + modifications: { 'tags' => [['visible', 'hidden', 'secret'], ['visible', 'hidden']] } + ) + end + + before do + SiteSetting.tagging_enabled = true + staff_tag_group + post.topic.tags = [public_tag2, hidden_tag] + end + + it 'returns all tag changes to staff' do + json = PostRevisionSerializer.new(post_revision, scope: Guardian.new(Fabricate(:admin)), root: false).as_json + expect(json[:tags_changes][:previous]).to include(public_tag.name) + expect(json[:tags_changes][:previous]).to include(hidden_tag.name) + expect(json[:tags_changes][:current]).to include(public_tag2.name) + expect(json[:tags_changes][:current]).to include(hidden_tag.name) + end + + it 'does not return hidden tags to non-staff' do + json = PostRevisionSerializer.new(post_revision, scope: Guardian.new(Fabricate(:user)), root: false).as_json + expect(json[:tags_changes][:previous]).to eq([public_tag.name]) + expect(json[:tags_changes][:current]).to eq([public_tag2.name]) + end + + it 'does not show tag modificiatons if changes are not visible to the user' do + json = PostRevisionSerializer.new(post_revision2, scope: Guardian.new(Fabricate(:user)), root: false).as_json + expect(json[:tags_changes]).to_not be_present + end + end +end diff --git a/spec/serializers/suggested_topic_serializer_spec.rb b/spec/serializers/suggested_topic_serializer_spec.rb index b9fcdb6d52..da29757bf9 100644 --- a/spec/serializers/suggested_topic_serializer_spec.rb +++ b/spec/serializers/suggested_topic_serializer_spec.rb @@ -2,11 +2,12 @@ require 'rails_helper' describe SuggestedTopicSerializer do let(:user) { Fabricate(:user) } + let(:admin) { Fabricate(:admin) } describe '#featured_link and #featured_link_root_domain' do let(:featured_link) { 'http://meta.discourse.org' } let(:topic) { Fabricate(:topic, featured_link: featured_link, category: Fabricate(:category, topic_featured_link_allowed: true)) } - subject(:json) { described_class.new(topic, scope: Guardian.new(user), root: false).as_json } + subject(:json) { SuggestedTopicSerializer.new(topic, scope: Guardian.new(user), root: false).as_json } context 'when topic featured link is disable' do before do @@ -32,4 +33,26 @@ describe SuggestedTopicSerializer do end end end + + describe 'hidden tags' do + let(:topic) { Fabricate(:topic) } + let(:hidden_tag) { Fabricate(:tag, name: 'hidden') } + let(:staff_tag_group) { Fabricate(:tag_group, permissions: { "staff" => 1 }, tag_names: [hidden_tag.name]) } + + before do + SiteSetting.tagging_enabled = true + staff_tag_group + topic.tags << hidden_tag + end + + it 'returns hidden tag to staff' do + json = SuggestedTopicSerializer.new(topic, scope: Guardian.new(admin), root: false).as_json + expect(json[:tags]).to eq([hidden_tag.name]) + end + + it 'does not return hidden tag to non-staff' do + json = SuggestedTopicSerializer.new(topic, scope: Guardian.new(user), root: false).as_json + expect(json[:tags]).to eq([]) + end + end end diff --git a/spec/serializers/topic_list_item_serializer_spec.rb b/spec/serializers/topic_list_item_serializer_spec.rb index 86f599e30d..319d8f02c6 100644 --- a/spec/serializers/topic_list_item_serializer_spec.rb +++ b/spec/serializers/topic_list_item_serializer_spec.rb @@ -43,4 +43,25 @@ describe TopicListItemSerializer do expect(serialized[:featured_link_root_domain]).to eq(nil) end end + + describe 'hidden tags' do + let(:hidden_tag) { Fabricate(:tag, name: 'hidden') } + let(:staff_tag_group) { Fabricate(:tag_group, permissions: { "staff" => 1 }, tag_names: [hidden_tag.name]) } + + before do + SiteSetting.tagging_enabled = true + staff_tag_group + topic.tags << hidden_tag + end + + it 'returns hidden tag to staff' do + json = TopicListItemSerializer.new(topic, scope: Guardian.new(Fabricate(:admin)), root: false).as_json + expect(json[:tags]).to eq([hidden_tag.name]) + end + + it 'does not return hidden tag to non-staff' do + json = TopicListItemSerializer.new(topic, scope: Guardian.new(Fabricate(:user)), root: false).as_json + expect(json[:tags]).to eq([]) + end + end end diff --git a/spec/serializers/topic_view_serializer_spec.rb b/spec/serializers/topic_view_serializer_spec.rb index a4b88a1a1c..8a6e27b056 100644 --- a/spec/serializers/topic_view_serializer_spec.rb +++ b/spec/serializers/topic_view_serializer_spec.rb @@ -1,13 +1,14 @@ require 'rails_helper' describe TopicViewSerializer do - def serialize_topic(topic, user) - topic_view = TopicView.new(topic.id, user) - described_class.new(topic_view, scope: Guardian.new(user), root: false).as_json + def serialize_topic(topic, user_arg) + topic_view = TopicView.new(topic.id, user_arg) + described_class.new(topic_view, scope: Guardian.new(user_arg), root: false).as_json end let(:topic) { Fabricate(:topic) } let(:user) { Fabricate(:user) } + let(:admin) { Fabricate(:admin) } describe '#featured_link and #featured_link_root_domain' do let(:featured_link) { 'http://meta.discourse.org' } @@ -69,7 +70,6 @@ describe TopicViewSerializer do describe 'when tags added to private message topics' do let(:moderator) { Fabricate(:moderator) } - let(:admin) { Fabricate(:admin) } let(:tag) { Fabricate(:tag) } let(:pm) do Fabricate(:private_message_topic, tags: [tag], topic_allowed_users: [ @@ -104,4 +104,25 @@ describe TopicViewSerializer do end end end + + describe 'with hidden tags' do + let(:hidden_tag) { Fabricate(:tag, name: 'hidden') } + let(:staff_tag_group) { Fabricate(:tag_group, permissions: { "staff" => 1 }, tag_names: [hidden_tag.name]) } + + before do + SiteSetting.tagging_enabled = true + staff_tag_group + topic.tags << hidden_tag + end + + it 'returns hidden tag to staff' do + json = serialize_topic(topic, admin) + expect(json[:tags]).to eq([hidden_tag.name]) + end + + it 'does not return hidden tag to non-staff' do + json = serialize_topic(topic, user) + expect(json[:tags]).to eq([]) + end + end end From 19c5afc69d660133e3dc4c625cc1d00628c06fb0 Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Wed, 21 Mar 2018 17:52:05 +0100 Subject: [PATCH 011/287] Protect against accidental table renames --- lib/migration/safe_migrate.rb | 12 ++++++------ spec/components/migration/safe_migrate_spec.rb | 16 ++++++++++++++++ .../rename_table/20990309014014_rename_table.rb | 9 +++++++++ 3 files changed, 31 insertions(+), 6 deletions(-) create mode 100644 spec/fixtures/migrate/rename_table/20990309014014_rename_table.rb diff --git a/lib/migration/safe_migrate.rb b/lib/migration/safe_migrate.rb index 92486fe67b..18e1e14d30 100644 --- a/lib/migration/safe_migrate.rb +++ b/lib/migration/safe_migrate.rb @@ -84,26 +84,26 @@ class Migration::SafeMigrate end def self.protect!(sql) - if sql =~ /^\s*drop\s+table/i + if sql =~ /^\s*(?:drop\s+table|alter\s+table.*rename\s+to)\s+/i $stdout.puts("", <<~STR) WARNING ------------------------------------------------------------------------------------- - An attempt was made to drop a table in a migration + An attempt was made to drop or rename a table in a migration SQL used was: '#{sql}' - Please use the deferred pattrn using Migration::TableDropper in db/seeds to drop - the table. + Please use the deferred pattern using Migration::TableDropper in db/seeds to drop + or rename the table. This protection is in place to protect us against dropping tables that are currently in use by live applications. STR raise Discourse::InvalidMigration, "Attempt was made to drop a table" - elsif sql =~ /^\s*alter\s+table.*(rename|drop)/i + elsif sql =~ /^\s*alter\s+table.*(?:rename|drop)\s+/i $stdout.puts("", <<~STR) WARNING ------------------------------------------------------------------------------------- An attempt was made to drop or rename a column in a migration SQL used was: '#{sql}' - Please use the deferred pattrn using Migration::ColumnDropper in db/seeds to drop + Please use the deferred pattern using Migration::ColumnDropper in db/seeds to drop or rename columns. Note, to minimize disruption use self.ignored_columns = ["column name"] on your diff --git a/spec/components/migration/safe_migrate_spec.rb b/spec/components/migration/safe_migrate_spec.rb index 846a4494cf..15ffd7d62a 100644 --- a/spec/components/migration/safe_migrate_spec.rb +++ b/spec/components/migration/safe_migrate_spec.rb @@ -37,6 +37,22 @@ describe Migration::SafeMigrate do expect(User.first).not_to eq(nil) end + it "bans all table renames" do + Migration::SafeMigrate.enable! + + path = File.expand_path "#{Rails.root}/spec/fixtures/migrate/rename_table" + + output = capture_stdout do + expect(lambda do + ActiveRecord::Migrator.up([path]) + end).to raise_error(StandardError) + end + + expect(output).to include("TableDropper") + + expect(User.first).not_to eq(nil) + end + it "bans all column removal" do Migration::SafeMigrate.enable! diff --git a/spec/fixtures/migrate/rename_table/20990309014014_rename_table.rb b/spec/fixtures/migrate/rename_table/20990309014014_rename_table.rb new file mode 100644 index 0000000000..7aca85e155 --- /dev/null +++ b/spec/fixtures/migrate/rename_table/20990309014014_rename_table.rb @@ -0,0 +1,9 @@ +class RenameTable < ActiveRecord::Migration[5.1] + def up + rename_table :users, :persons + end + + def down + raise "not tested" + end +end From cd17f609520f6cc8e71312c6ee0b867f42f41d5d Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Wed, 21 Mar 2018 18:06:44 +0100 Subject: [PATCH 012/287] Improve specs for accidental table/column drops and renames --- spec/components/migration/safe_migrate_spec.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spec/components/migration/safe_migrate_spec.rb b/spec/components/migration/safe_migrate_spec.rb index 15ffd7d62a..1be7f7c86d 100644 --- a/spec/components/migration/safe_migrate_spec.rb +++ b/spec/components/migration/safe_migrate_spec.rb @@ -34,6 +34,7 @@ describe Migration::SafeMigrate do expect(output).to include("TableDropper") + expect { User.first }.not_to raise_error expect(User.first).not_to eq(nil) end @@ -50,6 +51,7 @@ describe Migration::SafeMigrate do expect(output).to include("TableDropper") + expect { User.first }.not_to raise_error expect(User.first).not_to eq(nil) end @@ -67,6 +69,7 @@ describe Migration::SafeMigrate do expect(output).to include("ColumnDropper") expect(User.first).not_to eq(nil) + expect { User.first.username }.not_to raise_error end it "bans all column renames" do @@ -83,6 +86,7 @@ describe Migration::SafeMigrate do expect(output).to include("ColumnDropper") expect(User.first).not_to eq(nil) + expect { User.first.username }.not_to raise_error end it "supports being disabled" do From 4ad401bac540b33f9c3ddb9af02ae5aa77f344ad Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Mon, 26 Mar 2018 16:51:27 +0200 Subject: [PATCH 013/287] Ignore delay when first migration was < 10min ago --- lib/migration/base_dropper.rb | 7 +- .../migration/column_dropper_spec.rb | 116 +++++++---- .../migration/table_dropper_spec.rb | 191 +++++++++++++----- 3 files changed, 226 insertions(+), 88 deletions(-) diff --git a/lib/migration/base_dropper.rb b/lib/migration/base_dropper.rb index c00035d4ab..f36963d99e 100644 --- a/lib/migration/base_dropper.rb +++ b/lib/migration/base_dropper.rb @@ -33,7 +33,12 @@ module Migration SELECT 1 FROM schema_migration_details WHERE name = :after_migration AND - created_at <= (current_timestamp AT TIME ZONE 'UTC' - INTERVAL :delay) + (created_at <= (current_timestamp AT TIME ZONE 'UTC' - INTERVAL :delay) OR + (SELECT created_at + FROM schema_migration_details + ORDER BY id ASC + LIMIT 1) > (current_timestamp AT TIME ZONE 'UTC' - INTERVAL '10 minutes') + ) ) SQL end diff --git a/spec/components/migration/column_dropper_spec.rb b/spec/components/migration/column_dropper_spec.rb index 8d3c4ad6f3..6ae3f06f50 100644 --- a/spec/components/migration/column_dropper_spec.rb +++ b/spec/components/migration/column_dropper_spec.rb @@ -4,49 +4,97 @@ require_dependency 'migration/column_dropper' RSpec.describe Migration::ColumnDropper do def has_column?(table, column) - Topic.exec_sql("SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS - WHERE - table_schema = 'public' AND - table_name = :table AND - column_name = :column - ", - table: table, column: column - ).to_a.length == 1 + ActiveRecord::Base.exec_sql(<<~SQL, table: table, column: column).to_a.length == 1 + SELECT 1 + FROM INFORMATION_SCHEMA.COLUMNS + WHERE + table_schema = 'public' AND + table_name = :table AND + column_name = :column + SQL end - it "can correctly drop columns after correct delay" do - Topic.exec_sql "ALTER TABLE topics ADD COLUMN junk int" - name = Topic - .exec_sql("SELECT name FROM schema_migration_details LIMIT 1") - .getvalue(0, 0) + def update_first_migration_date(created_at) + ActiveRecord::Base.exec_sql(<<~SQL, created_at: created_at) + UPDATE schema_migration_details + SET created_at = :created_at + WHERE id = (SELECT MIN(id) + FROM schema_migration_details) + SQL + end - Topic.exec_sql("UPDATE schema_migration_details SET created_at = :created_at WHERE name = :name", - name: name, created_at: 15.minutes.ago) + describe ".drop" do + let(:migration_name) do + ActiveRecord::Base + .exec_sql("SELECT name FROM schema_migration_details ORDER BY id DESC LIMIT 1") + .getvalue(0, 0) + end - dropped_proc_called = false + before do + Topic.exec_sql "ALTER TABLE topics ADD COLUMN junk int" - Migration::ColumnDropper.drop( - table: 'topics', - after_migration: name, - columns: ['junk'], - delay: 20.minutes, - on_drop: ->() { dropped_proc_called = true } - ) + ActiveRecord::Base.exec_sql(<<~SQL, name: migration_name, created_at: 15.minutes.ago) + UPDATE schema_migration_details + SET created_at = :created_at + WHERE name = :name + SQL + end - expect(has_column?('topics', 'junk')).to eq(true) - expect(dropped_proc_called).to eq(false) + it "can correctly drop columns after correct delay" do + dropped_proc_called = false + update_first_migration_date(2.years.ago) - Migration::ColumnDropper.drop( - table: 'topics', - after_migration: name, - columns: ['junk'], - delay: 10.minutes, - on_drop: ->() { dropped_proc_called = true } - ) + Migration::ColumnDropper.drop( + table: 'topics', + after_migration: migration_name, + columns: ['junk'], + delay: 20.minutes, + on_drop: ->() { dropped_proc_called = true } + ) - expect(has_column?('topics', 'junk')).to eq(false) - expect(dropped_proc_called).to eq(true) + expect(has_column?('topics', 'junk')).to eq(true) + expect(dropped_proc_called).to eq(false) + Migration::ColumnDropper.drop( + table: 'topics', + after_migration: migration_name, + columns: ['junk'], + delay: 10.minutes, + on_drop: ->() { dropped_proc_called = true } + ) + + expect(has_column?('topics', 'junk')).to eq(false) + expect(dropped_proc_called).to eq(true) + end + + it "drops the columns immediately if the first migration was less than 10 minutes ago" do + dropped_proc_called = false + update_first_migration_date(11.minutes.ago) + + Migration::ColumnDropper.drop( + table: 'topics', + after_migration: migration_name, + columns: ['junk'], + delay: 30.minutes, + on_drop: ->() { dropped_proc_called = true } + ) + + expect(has_column?('topics', 'junk')).to eq(true) + expect(dropped_proc_called).to eq(false) + + update_first_migration_date(9.minutes.ago) + + Migration::ColumnDropper.drop( + table: 'topics', + after_migration: migration_name, + columns: ['junk'], + delay: 30.minutes, + on_drop: ->() { dropped_proc_called = true } + ) + + expect(has_column?('topics', 'junk')).to eq(false) + expect(dropped_proc_called).to eq(true) + end end describe '.mark_readonly' do diff --git a/spec/components/migration/table_dropper_spec.rb b/spec/components/migration/table_dropper_spec.rb index 3dcaf8c5d8..e6dc4a1a06 100644 --- a/spec/components/migration/table_dropper_spec.rb +++ b/spec/components/migration/table_dropper_spec.rb @@ -14,83 +14,168 @@ describe Migration::TableDropper do ActiveRecord::Base.exec_sql(sql).to_a.length > 0 end + def update_first_migration_date(created_at) + ActiveRecord::Base.exec_sql(<<~SQL, created_at: created_at) + UPDATE schema_migration_details + SET created_at = :created_at + WHERE id = (SELECT MIN(id) + FROM schema_migration_details) + SQL + end + + def create_new_table + ActiveRecord::Base.exec_sql "CREATE TABLE table_with_new_name (topic_id INTEGER)" + end + let(:migration_name) do ActiveRecord::Base - .exec_sql("SELECT name FROM schema_migration_details LIMIT 1") + .exec_sql("SELECT name FROM schema_migration_details ORDER BY id DESC LIMIT 1") .getvalue(0, 0) end before do ActiveRecord::Base.exec_sql "CREATE TABLE table_with_old_name (topic_id INTEGER)" - Topic.exec_sql("UPDATE schema_migration_details SET created_at = :created_at WHERE name = :name", - name: migration_name, created_at: 15.minutes.ago) + ActiveRecord::Base.exec_sql(<<~SQL, name: migration_name, created_at: 15.minutes.ago) + UPDATE schema_migration_details + SET created_at = :created_at + WHERE name = :name + SQL end - describe "#delayed_rename" do - it "can drop a table after correct delay and when new table exists" do - dropped_proc_called = false + context "first migration was a long time ago" do + before do + update_first_migration_date(2.years.ago) + end - described_class.delayed_rename( - old_name: 'table_with_old_name', - new_name: 'table_with_new_name', - after_migration: migration_name, - delay: 20.minutes, - on_drop: ->() { dropped_proc_called = true } - ) + describe ".delayed_rename" do + it "can drop a table after correct delay and when new table exists" do + dropped_proc_called = false - expect(table_exists?('table_with_old_name')).to eq(true) - expect(dropped_proc_called).to eq(false) + described_class.delayed_rename( + old_name: 'table_with_old_name', + new_name: 'table_with_new_name', + after_migration: migration_name, + delay: 20.minutes, + on_drop: ->() { dropped_proc_called = true } + ) - described_class.delayed_rename( - old_name: 'table_with_old_name', - new_name: 'table_with_new_name', - after_migration: migration_name, - delay: 10.minutes, - on_drop: ->() { dropped_proc_called = true } - ) + expect(table_exists?('table_with_old_name')).to eq(true) + expect(dropped_proc_called).to eq(false) - expect(table_exists?('table_with_old_name')).to eq(true) - expect(dropped_proc_called).to eq(false) + described_class.delayed_rename( + old_name: 'table_with_old_name', + new_name: 'table_with_new_name', + after_migration: migration_name, + delay: 10.minutes, + on_drop: ->() { dropped_proc_called = true } + ) - ActiveRecord::Base.exec_sql "CREATE TABLE table_with_new_name (topic_id INTEGER)" + expect(table_exists?('table_with_old_name')).to eq(true) + expect(dropped_proc_called).to eq(false) - described_class.delayed_rename( - old_name: 'table_with_old_name', - new_name: 'table_with_new_name', - after_migration: migration_name, - delay: 10.minutes, - on_drop: ->() { dropped_proc_called = true } - ) + create_new_table - expect(table_exists?('table_with_old_name')).to eq(false) - expect(dropped_proc_called).to eq(true) + described_class.delayed_rename( + old_name: 'table_with_old_name', + new_name: 'table_with_new_name', + after_migration: migration_name, + delay: 10.minutes, + on_drop: ->() { dropped_proc_called = true } + ) + + expect(table_exists?('table_with_old_name')).to eq(false) + expect(dropped_proc_called).to eq(true) + end + end + + describe ".delayed_drop" do + it "can drop a table after correct delay" do + dropped_proc_called = false + + described_class.delayed_drop( + table_name: 'table_with_old_name', + after_migration: migration_name, + delay: 20.minutes, + on_drop: ->() { dropped_proc_called = true } + ) + + expect(table_exists?('table_with_old_name')).to eq(true) + expect(dropped_proc_called).to eq(false) + + described_class.delayed_drop( + table_name: 'table_with_old_name', + after_migration: migration_name, + delay: 10.minutes, + on_drop: ->() { dropped_proc_called = true } + ) + + expect(table_exists?('table_with_old_name')).to eq(false) + expect(dropped_proc_called).to eq(true) + end end end - describe "#delayed_drop" do - it "can drop a table after correct delay" do - dropped_proc_called = false + context "first migration was a less than 10 minutes ago" do + describe ".delayed_rename" do + it "can drop a table after correct delay and when new table exists" do + dropped_proc_called = false + update_first_migration_date(11.minutes.ago) + create_new_table - described_class.delayed_drop( - table_name: 'table_with_old_name', - after_migration: migration_name, - delay: 20.minutes, - on_drop: ->() { dropped_proc_called = true } - ) + described_class.delayed_rename( + old_name: 'table_with_old_name', + new_name: 'table_with_new_name', + after_migration: migration_name, + delay: 30.minutes, + on_drop: ->() { dropped_proc_called = true } + ) - expect(table_exists?('table_with_old_name')).to eq(true) - expect(dropped_proc_called).to eq(false) + expect(table_exists?('table_with_old_name')).to eq(true) + expect(dropped_proc_called).to eq(false) - described_class.delayed_drop( - table_name: 'table_with_old_name', - after_migration: migration_name, - delay: 10.minutes, - on_drop: ->() { dropped_proc_called = true } - ) + update_first_migration_date(9.minutes.ago) - expect(table_exists?('table_with_old_name')).to eq(false) - expect(dropped_proc_called).to eq(true) + described_class.delayed_rename( + old_name: 'table_with_old_name', + new_name: 'table_with_new_name', + after_migration: migration_name, + delay: 30.minutes, + on_drop: ->() { dropped_proc_called = true } + ) + + expect(table_exists?('table_with_old_name')).to eq(false) + expect(dropped_proc_called).to eq(true) + end + end + + describe ".delayed_drop" do + it "immediately drops the table" do + dropped_proc_called = false + update_first_migration_date(11.minutes.ago) + + described_class.delayed_drop( + table_name: 'table_with_old_name', + after_migration: migration_name, + delay: 30.minutes, + on_drop: ->() { dropped_proc_called = true } + ) + + expect(table_exists?('table_with_old_name')).to eq(true) + expect(dropped_proc_called).to eq(false) + + update_first_migration_date(9.minutes.ago) + + described_class.delayed_drop( + table_name: 'table_with_old_name', + after_migration: migration_name, + delay: 30.minutes, + on_drop: ->() { dropped_proc_called = true } + ) + + expect(table_exists?('table_with_old_name')).to eq(false) + expect(dropped_proc_called).to eq(true) + end end end end From b945a2dc39fb462bcb001be607d9c16f2b856193 Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Mon, 26 Mar 2018 17:05:18 +0200 Subject: [PATCH 014/287] Call `on_drop` only when tables/columns are dropped --- lib/migration/table_dropper.rb | 7 +++--- .../migration/column_dropper_spec.rb | 13 ++++++++++ .../migration/table_dropper_spec.rb | 25 +++++++++++++++++++ 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/lib/migration/table_dropper.rb b/lib/migration/table_dropper.rb index 02d383849e..78a9c7172f 100644 --- a/lib/migration/table_dropper.rb +++ b/lib/migration/table_dropper.rb @@ -44,9 +44,10 @@ module Migration LIMIT 1 SQL - builder.where(new_table_exists) if @new_name.present? + builder.where(table_exists(":new_name")) if @new_name.present? builder.where("table_schema = 'public'") + .where(table_exists(":old_name")) .where(previous_migration_done) .exec(old_name: @old_name, new_name: @new_name, @@ -54,13 +55,13 @@ module Migration after_migration: @after_migration).to_a.length > 0 end - def new_table_exists + def table_exists(table_name_placeholder) <<~SQL EXISTS( SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE table_schema = 'public' AND - table_name = :new_name + table_name = #{table_name_placeholder} ) SQL end diff --git a/spec/components/migration/column_dropper_spec.rb b/spec/components/migration/column_dropper_spec.rb index 6ae3f06f50..9a0bd536e2 100644 --- a/spec/components/migration/column_dropper_spec.rb +++ b/spec/components/migration/column_dropper_spec.rb @@ -65,6 +65,19 @@ RSpec.describe Migration::ColumnDropper do expect(has_column?('topics', 'junk')).to eq(false) expect(dropped_proc_called).to eq(true) + + dropped_proc_called = false + + Migration::ColumnDropper.drop( + table: 'topics', + after_migration: migration_name, + columns: ['junk'], + delay: 10.minutes, + on_drop: ->() { dropped_proc_called = true } + ) + + # it should call "on_drop" only when there are columns to drop + expect(dropped_proc_called).to eq(false) end it "drops the columns immediately if the first migration was less than 10 minutes ago" do diff --git a/spec/components/migration/table_dropper_spec.rb b/spec/components/migration/table_dropper_spec.rb index e6dc4a1a06..0b798f82bc 100644 --- a/spec/components/migration/table_dropper_spec.rb +++ b/spec/components/migration/table_dropper_spec.rb @@ -86,6 +86,19 @@ describe Migration::TableDropper do expect(table_exists?('table_with_old_name')).to eq(false) expect(dropped_proc_called).to eq(true) + + dropped_proc_called = false + + described_class.delayed_rename( + old_name: 'table_with_old_name', + new_name: 'table_with_new_name', + after_migration: migration_name, + delay: 10.minutes, + on_drop: ->() { dropped_proc_called = true } + ) + + # it should call "on_drop" only when there is a table to drop + expect(dropped_proc_called).to eq(false) end end @@ -112,6 +125,18 @@ describe Migration::TableDropper do expect(table_exists?('table_with_old_name')).to eq(false) expect(dropped_proc_called).to eq(true) + + dropped_proc_called = false + + described_class.delayed_drop( + table_name: 'table_with_old_name', + after_migration: migration_name, + delay: 10.minutes, + on_drop: ->() { dropped_proc_called = true } + ) + + # it should call "on_drop" only when there is a table to drop + expect(dropped_proc_called).to eq(false) end end end From 2ecd234e277d7d2f250d22da2fc0005d41561d2f Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 27 Mar 2018 12:18:03 +0800 Subject: [PATCH 015/287] UX: Consolidation group manangement into a single tab. --- .../javascripts/admin/templates/group.hbs | 4 +- ...js.es6 => group-manage-logs-filter.js.es6} | 2 +- ...ow.js.es6 => group-manage-logs-row.js.es6} | 0 .../components/group-members-input.js.es6 | 2 +- .../group-navigation-dropdown.js.es6 | 25 ------- ...p-logs.js.es6 => group-manage-logs.js.es6} | 0 ...hip.js.es6 => group-manage-members.js.es6} | 16 +++-- ...dit.js.es6 => group-manage-profile.js.es6} | 10 +-- .../discourse/controllers/group-manage.js.es6 | 9 +++ .../discourse/controllers/group.js.es6 | 9 +-- .../discourse/routes/app-route-map.js.es6 | 7 +- .../routes/group-manage-index.js.es6 | 5 ++ ...p-logs.js.es6 => group-manage-logs.js.es6} | 7 +- .../routes/group-manage-members.js.es6 | 9 +++ .../routes/group-manage-profile.js.es6 | 9 +++ ...{group-edit.js.es6 => group-manage.js.es6} | 7 +- .../javascripts/discourse/routes/group.js.es6 | 8 --- ...ilter.hbs => group-manage-logs-filter.hbs} | 2 +- ...logs-row.hbs => group-manage-logs-row.hbs} | 8 +-- .../components/group-members-input.hbs | 2 +- .../discourse/templates/group-edit.hbs | 66 ----------------- .../discourse/templates/group-logs.hbs | 33 --------- .../javascripts/discourse/templates/group.hbs | 6 -- .../discourse/templates/group/manage.hbs | 15 ++++ .../discourse/templates/group/manage/logs.hbs | 33 +++++++++ .../templates/group/manage/members.hbs | 25 +++++++ .../templates/group/manage/profile.hbs | 71 +++++++++++++++++++ .../templates/modal/group-membership.hbs | 25 ------- app/assets/stylesheets/common/base/group.scss | 20 ++---- .../base/request-group-membership-form.scss | 2 +- app/assets/stylesheets/desktop/group.scss | 15 ++-- app/assets/stylesheets/mobile/group.scss | 9 ++- app/controllers/groups_controller.rb | 10 ++- config/locales/client.en.yml | 37 +++++----- config/routes.rb | 4 ++ spec/requests/groups_controller_spec.rb | 18 +++-- .../acceptance/group-edit-test.js.es6 | 61 ---------------- .../acceptance/group-index-test.js.es6 | 29 -------- ...t.js.es6 => group-manage-logs-test.js.es6} | 12 ++-- .../group-manage-profile-test.js.es6 | 62 ++++++++++++++++ .../javascripts/acceptance/groups-test.js.es6 | 7 +- 41 files changed, 355 insertions(+), 346 deletions(-) rename app/assets/javascripts/discourse/components/{group-logs-filter.js.es6 => group-manage-logs-filter.js.es6} (89%) rename app/assets/javascripts/discourse/components/{group-logs-row.js.es6 => group-manage-logs-row.js.es6} (100%) delete mode 100644 app/assets/javascripts/discourse/components/group-navigation-dropdown.js.es6 rename app/assets/javascripts/discourse/controllers/{group-logs.js.es6 => group-manage-logs.js.es6} (100%) rename app/assets/javascripts/discourse/controllers/{group-membership.js.es6 => group-manage-members.js.es6} (66%) rename app/assets/javascripts/discourse/controllers/{group-edit.js.es6 => group-manage-profile.js.es6} (72%) create mode 100644 app/assets/javascripts/discourse/controllers/group-manage.js.es6 create mode 100644 app/assets/javascripts/discourse/routes/group-manage-index.js.es6 rename app/assets/javascripts/discourse/routes/{group-logs.js.es6 => group-manage-logs.js.es6} (51%) create mode 100644 app/assets/javascripts/discourse/routes/group-manage-members.js.es6 create mode 100644 app/assets/javascripts/discourse/routes/group-manage-profile.js.es6 rename app/assets/javascripts/discourse/routes/{group-edit.js.es6 => group-manage.js.es6} (63%) rename app/assets/javascripts/discourse/templates/components/{group-logs-filter.hbs => group-manage-logs-filter.hbs} (53%) rename app/assets/javascripts/discourse/templates/components/{group-logs-row.hbs => group-manage-logs-row.hbs} (80%) delete mode 100644 app/assets/javascripts/discourse/templates/group-edit.hbs delete mode 100644 app/assets/javascripts/discourse/templates/group-logs.hbs create mode 100644 app/assets/javascripts/discourse/templates/group/manage.hbs create mode 100644 app/assets/javascripts/discourse/templates/group/manage/logs.hbs create mode 100644 app/assets/javascripts/discourse/templates/group/manage/members.hbs create mode 100644 app/assets/javascripts/discourse/templates/group/manage/profile.hbs delete mode 100644 app/assets/javascripts/discourse/templates/modal/group-membership.hbs delete mode 100644 test/javascripts/acceptance/group-edit-test.js.es6 rename test/javascripts/acceptance/{group-logs-test.js.es6 => group-manage-logs-test.js.es6} (85%) create mode 100644 test/javascripts/acceptance/group-manage-profile-test.js.es6 diff --git a/app/assets/javascripts/admin/templates/group.hbs b/app/assets/javascripts/admin/templates/group.hbs index c10ca20bb3..53ae2b748e 100644 --- a/app/assets/javascripts/admin/templates/group.hbs +++ b/app/assets/javascripts/admin/templates/group.hbs @@ -11,8 +11,8 @@ {{#unless model.automatic}}
- - {{input type='text' name='full_name' value=model.full_name class='group-edit-full-name'}} + + {{input type='text' name='full_name' value=model.full_name class='group-manage-full-name'}}
diff --git a/app/assets/javascripts/discourse/components/group-logs-filter.js.es6 b/app/assets/javascripts/discourse/components/group-manage-logs-filter.js.es6 similarity index 89% rename from app/assets/javascripts/discourse/components/group-logs-filter.js.es6 rename to app/assets/javascripts/discourse/components/group-manage-logs-filter.js.es6 index ba4ccc3f74..d598bc568f 100644 --- a/app/assets/javascripts/discourse/components/group-logs-filter.js.es6 +++ b/app/assets/javascripts/discourse/components/group-manage-logs-filter.js.es6 @@ -5,7 +5,7 @@ export default Ember.Component.extend({ @computed('type') label(type) { - return I18n.t(`groups.logs.${type}`); + return I18n.t(`groups.manage.logs.${type}`); }, @computed('value', 'type') diff --git a/app/assets/javascripts/discourse/components/group-logs-row.js.es6 b/app/assets/javascripts/discourse/components/group-manage-logs-row.js.es6 similarity index 100% rename from app/assets/javascripts/discourse/components/group-logs-row.js.es6 rename to app/assets/javascripts/discourse/components/group-manage-logs-row.js.es6 diff --git a/app/assets/javascripts/discourse/components/group-members-input.js.es6 b/app/assets/javascripts/discourse/components/group-members-input.js.es6 index 691eb051bb..1bb3ca048d 100644 --- a/app/assets/javascripts/discourse/components/group-members-input.js.es6 +++ b/app/assets/javascripts/discourse/components/group-members-input.js.es6 @@ -55,7 +55,7 @@ export default Ember.Component.extend({ }, removeMember(member) { - const message = I18n.t("groups.edit.delete_member_confirm",{ + const message = I18n.t("groups.manage.delete_member_confirm",{ username: member.get("username"), group: this.get("model.name") }); diff --git a/app/assets/javascripts/discourse/components/group-navigation-dropdown.js.es6 b/app/assets/javascripts/discourse/components/group-navigation-dropdown.js.es6 deleted file mode 100644 index 263fc426e4..0000000000 --- a/app/assets/javascripts/discourse/components/group-navigation-dropdown.js.es6 +++ /dev/null @@ -1,25 +0,0 @@ -import DropdownSelectBox from "select-kit/components/dropdown-select-box"; - -export default DropdownSelectBox.extend({ - classNames: ["group-navigation-dropdown", "pull-right"], - nameProperty: "label", - headerIcon: ["bars"], - showFullTitle: false, - - computeContent() { - const content = []; - - content.push({ - id: "manageMembership", - icon: "user-plus", - label: I18n.t("groups.add_members.title"), - description: I18n.t("groups.add_members.description"), - }); - - return content; - }, - - mutateValue(value) { - this.get(value)(this.get('model')); - } -}); diff --git a/app/assets/javascripts/discourse/controllers/group-logs.js.es6 b/app/assets/javascripts/discourse/controllers/group-manage-logs.js.es6 similarity index 100% rename from app/assets/javascripts/discourse/controllers/group-logs.js.es6 rename to app/assets/javascripts/discourse/controllers/group-manage-logs.js.es6 diff --git a/app/assets/javascripts/discourse/controllers/group-membership.js.es6 b/app/assets/javascripts/discourse/controllers/group-manage-members.js.es6 similarity index 66% rename from app/assets/javascripts/discourse/controllers/group-membership.js.es6 rename to app/assets/javascripts/discourse/controllers/group-manage-members.js.es6 index 8c22d2765e..71ee3f8a8f 100644 --- a/app/assets/javascripts/discourse/controllers/group-membership.js.es6 +++ b/app/assets/javascripts/discourse/controllers/group-manage-members.js.es6 @@ -1,18 +1,19 @@ -import ModalFunctionality from 'discourse/mixins/modal-functionality'; import computed from 'ember-addons/ember-computed-decorators'; -import { extractError } from 'discourse/lib/ajax-error'; +import { popupAjaxError } from 'discourse/lib/ajax-error'; -export default Ember.Controller.extend(ModalFunctionality, { +export default Ember.Controller.extend({ loading: false, setAsOwner: false, - @computed('model.usernames') - disableAddButton(usernames) { - return !usernames || !(usernames.length > 0); + @computed('model.usernames', 'loading') + disableAddButton(usernames, loading) { + return loading || !usernames || !(usernames.length > 0); }, actions: { addMembers() { + this.set('loading', true); + const model = this.get('model'); const usernames = model.get('usernames'); if (Em.isEmpty(usernames)) { return; } @@ -33,7 +34,8 @@ export default Ember.Controller.extend(ModalFunctionality, { model.set("usernames", null); this.send('closeModal'); }) - .catch(error => this.flash(extractError(error), 'error')); + .catch(popupAjaxError) + .finally(() => this.set('loading', false)); }, }, }); diff --git a/app/assets/javascripts/discourse/controllers/group-edit.js.es6 b/app/assets/javascripts/discourse/controllers/group-manage-profile.js.es6 similarity index 72% rename from app/assets/javascripts/discourse/controllers/group-edit.js.es6 rename to app/assets/javascripts/discourse/controllers/group-manage-profile.js.es6 index 32277430af..0c8bec50be 100644 --- a/app/assets/javascripts/discourse/controllers/group-edit.js.es6 +++ b/app/assets/javascripts/discourse/controllers/group-manage-profile.js.es6 @@ -13,11 +13,11 @@ export default Ember.Controller.extend({ save() { this.set('saving', true); - this.get('model').save().catch(error => { - popupAjaxError(error); - }).finally(() => { - this.set('saving', false); - }); + this.get('model').save() + .catch(popupAjaxError) + .finally(() => { + this.set('saving', false); + }); } } }); diff --git a/app/assets/javascripts/discourse/controllers/group-manage.js.es6 b/app/assets/javascripts/discourse/controllers/group-manage.js.es6 new file mode 100644 index 0000000000..3b94a86d02 --- /dev/null +++ b/app/assets/javascripts/discourse/controllers/group-manage.js.es6 @@ -0,0 +1,9 @@ +export default Ember.Controller.extend({ + application: Ember.inject.controller(), + + tabs: [ + { route: 'group.manage.profile', title: 'groups.manage.profile.title' }, + { route: 'group.manage.members', title: 'groups.manage.members.title' }, + { route: 'group.manage.logs', title: 'groups.manage.logs.title' }, + ], +}); diff --git a/app/assets/javascripts/discourse/controllers/group.js.es6 b/app/assets/javascripts/discourse/controllers/group.js.es6 index 46faf5ae8b..d17de48bec 100644 --- a/app/assets/javascripts/discourse/controllers/group.js.es6 +++ b/app/assets/javascripts/discourse/controllers/group.js.es6 @@ -37,14 +37,11 @@ export default Ember.Controller.extend({ } if (this.currentUser && this.currentUser.canManageGroup(this.model)) { - defaultTabs.push(...[ + defaultTabs.push( Tab.create({ - name: 'edit', i18nKey: 'edit.title', icon: 'pencil' - }), - Tab.create({ - name: 'logs', i18nKey: 'logs.title', icon: 'list-alt' + name: 'manage', i18nKey: 'manage.title', icon: 'wrench' }) - ]); + ); } return defaultTabs; diff --git a/app/assets/javascripts/discourse/routes/app-route-map.js.es6 b/app/assets/javascripts/discourse/routes/app-route-map.js.es6 index 4ed43977d7..524e851001 100644 --- a/app/assets/javascripts/discourse/routes/app-route-map.js.es6 +++ b/app/assets/javascripts/discourse/routes/app-route-map.js.es6 @@ -60,8 +60,11 @@ export default function() { this.route('mentions'); }); - this.route('logs'); - this.route('edit'); + this.route('manage', function() { + this.route('profile'); + this.route('members'); + this.route('logs'); + }); this.route('messages', function() { this.route('inbox'); diff --git a/app/assets/javascripts/discourse/routes/group-manage-index.js.es6 b/app/assets/javascripts/discourse/routes/group-manage-index.js.es6 new file mode 100644 index 0000000000..f5b26ccd0c --- /dev/null +++ b/app/assets/javascripts/discourse/routes/group-manage-index.js.es6 @@ -0,0 +1,5 @@ +export default Discourse.Route.extend({ + beforeModel() { + this.transitionTo("group.manage.profile"); + } +}); diff --git a/app/assets/javascripts/discourse/routes/group-logs.js.es6 b/app/assets/javascripts/discourse/routes/group-manage-logs.js.es6 similarity index 51% rename from app/assets/javascripts/discourse/routes/group-logs.js.es6 rename to app/assets/javascripts/discourse/routes/group-manage-logs.js.es6 index 9658160fee..85ee4dfae9 100644 --- a/app/assets/javascripts/discourse/routes/group-logs.js.es6 +++ b/app/assets/javascripts/discourse/routes/group-manage-logs.js.es6 @@ -1,6 +1,6 @@ export default Discourse.Route.extend({ titleToken() { - return I18n.t('groups.logs.title'); + return I18n.t('groups.manage.logs.title'); }, model() { @@ -8,13 +8,12 @@ export default Discourse.Route.extend({ }, setupController(controller, model) { - this.controllerFor('group-logs').setProperties({ model }); - this.controllerFor("group").set("showing", 'logs'); + this.controllerFor('group-manage-logs').setProperties({ model }); }, actions: { willTransition() { - this.controllerFor('group-logs').reset(); + this.controllerFor('group-manage-logs').reset(); } } }); diff --git a/app/assets/javascripts/discourse/routes/group-manage-members.js.es6 b/app/assets/javascripts/discourse/routes/group-manage-members.js.es6 new file mode 100644 index 0000000000..16bec11a46 --- /dev/null +++ b/app/assets/javascripts/discourse/routes/group-manage-members.js.es6 @@ -0,0 +1,9 @@ +export default Discourse.Route.extend({ + titleToken() { + return I18n.t('groups.manage.members.title'); + }, + + model() { + return this.modelFor('group'); + }, +}); diff --git a/app/assets/javascripts/discourse/routes/group-manage-profile.js.es6 b/app/assets/javascripts/discourse/routes/group-manage-profile.js.es6 new file mode 100644 index 0000000000..8c25f3b54b --- /dev/null +++ b/app/assets/javascripts/discourse/routes/group-manage-profile.js.es6 @@ -0,0 +1,9 @@ +export default Discourse.Route.extend({ + titleToken() { + return I18n.t('groups.manage.profile.title'); + }, + + model() { + return this.modelFor('group'); + }, +}); diff --git a/app/assets/javascripts/discourse/routes/group-edit.js.es6 b/app/assets/javascripts/discourse/routes/group-manage.js.es6 similarity index 63% rename from app/assets/javascripts/discourse/routes/group-edit.js.es6 rename to app/assets/javascripts/discourse/routes/group-manage.js.es6 index 7f3dc02cec..aca7a4bc6e 100644 --- a/app/assets/javascripts/discourse/routes/group-edit.js.es6 +++ b/app/assets/javascripts/discourse/routes/group-manage.js.es6 @@ -1,6 +1,6 @@ export default Discourse.Route.extend({ titleToken() { - return I18n.t('groups.edit.title'); + return I18n.t('groups.manage.title'); }, model() { @@ -14,8 +14,7 @@ export default Discourse.Route.extend({ }, setupController(controller, model) { - this.controllerFor('group-edit').setProperties({ model }); - this.controllerFor("group").set("showing", 'edit'); - model.findMembers(); + this.controllerFor('group-manage').setProperties({ model }); + this.controllerFor("group").set("showing", 'manage'); } }); diff --git a/app/assets/javascripts/discourse/routes/group.js.es6 b/app/assets/javascripts/discourse/routes/group.js.es6 index 6bb9610aed..3df20f794b 100644 --- a/app/assets/javascripts/discourse/routes/group.js.es6 +++ b/app/assets/javascripts/discourse/routes/group.js.es6 @@ -1,5 +1,4 @@ import Group from 'discourse/models/group'; -import showModal from 'discourse/lib/show-modal'; export default Discourse.Route.extend({ @@ -17,12 +16,5 @@ export default Discourse.Route.extend({ setupController(controller, model) { controller.setProperties({ model, counts: this.get('counts') }); - }, - - actions: { - showGroupMembershipModal(model) { - showModal('group-membership', { model }); - this.controllerFor('modal').set('modalClass', 'group-membership-modal'); - }, } }); diff --git a/app/assets/javascripts/discourse/templates/components/group-logs-filter.hbs b/app/assets/javascripts/discourse/templates/components/group-manage-logs-filter.hbs similarity index 53% rename from app/assets/javascripts/discourse/templates/components/group-logs-filter.hbs rename to app/assets/javascripts/discourse/templates/components/group-manage-logs-filter.hbs index f9f13474c7..1625fa5fc9 100644 --- a/app/assets/javascripts/discourse/templates/components/group-logs-filter.hbs +++ b/app/assets/javascripts/discourse/templates/components/group-manage-logs-filter.hbs @@ -1,5 +1,5 @@ {{#if value}} - {{#d-button class="btn-small group-logs-filter" action="clearFilter" actionParam=type}} + {{#d-button class="btn-small group-manage-logs-filter" action="clearFilter" actionParam=type}} {{label}}: {{filterText}} {{d-icon "times-circle"}} {{/d-button}} diff --git a/app/assets/javascripts/discourse/templates/components/group-logs-row.hbs b/app/assets/javascripts/discourse/templates/components/group-manage-logs-row.hbs similarity index 80% rename from app/assets/javascripts/discourse/templates/components/group-logs-row.hbs rename to app/assets/javascripts/discourse/templates/components/group-manage-logs-row.hbs index 6fe9bfd858..da955686e1 100644 --- a/app/assets/javascripts/discourse/templates/components/group-logs-row.hbs +++ b/app/assets/javascripts/discourse/templates/components/group-manage-logs-row.hbs @@ -1,4 +1,4 @@ - + {{#d-button class="btn-small" action="filter" actionParam=(hash value=log.action key="action")}} {{log.actionTitle}} @@ -33,7 +33,7 @@ {{bound-date log.created_at}} - + {{#if log.prev_value}} {{#if expandDetails}} {{d-icon 'ellipsis-v'}} @@ -48,11 +48,11 @@

- {{i18n 'groups.logs.from'}}: {{log.prev_value}} + {{i18n 'groups.manage.logs.from'}}: {{log.prev_value}}

- {{i18n 'groups.logs.to'}}: {{log.new_value}} + {{i18n 'groups.manage.logs.to'}}: {{log.new_value}}

diff --git a/app/assets/javascripts/discourse/templates/components/group-members-input.hbs b/app/assets/javascripts/discourse/templates/components/group-members-input.hbs index fc0e9667b8..036bf9b1ca 100644 --- a/app/assets/javascripts/discourse/templates/components/group-members-input.hbs +++ b/app/assets/javascripts/discourse/templates/components/group-members-input.hbs @@ -24,7 +24,7 @@ class="add" icon="plus" disabled=disableAddButton - label="groups.edit.add_members"}} + label="groups.manage.add_members"}} {{/if}}
{{/unless}} diff --git a/app/assets/javascripts/discourse/templates/group-edit.hbs b/app/assets/javascripts/discourse/templates/group-edit.hbs deleted file mode 100644 index 7e56a0ead2..0000000000 --- a/app/assets/javascripts/discourse/templates/group-edit.hbs +++ /dev/null @@ -1,66 +0,0 @@ -
-
-
- - {{input type='text' name='full_name' value=model.full_name class='group-edit-full-name'}} -
- -
- - {{d-editor value=model.bio_raw class="group-edit-bio"}} -
- -
- {{group-flair-inputs model=model}} -
- -
- -
- -
- -
- -
- -
- - {{#if model.allow_membership_requests}} -
- - - {{expanding-text-area name="membership-request-template" - value=model.membership_request_template - class="group-edit-membership-request-template"}} -
- {{/if}} - - {{plugin-outlet name="group-edit" args=(hash group=model)}} - - {{d-button action="save" class="btn-primary" disabled=saving label="save"}} - {{savingText}} -
-
diff --git a/app/assets/javascripts/discourse/templates/group-logs.hbs b/app/assets/javascripts/discourse/templates/group-logs.hbs deleted file mode 100644 index 154b78d678..0000000000 --- a/app/assets/javascripts/discourse/templates/group-logs.hbs +++ /dev/null @@ -1,33 +0,0 @@ -{{#if model.logs}} -
- {{group-logs-filter clearFilter="clearFilter" value=filters.action type="action"}} - {{group-logs-filter clearFilter="clearFilter" value=filters.acting_user type="acting_user"}} - {{group-logs-filter clearFilter="clearFilter" value=filters.target_user type="target_user"}} - {{group-logs-filter clearFilter="clearFilter" value=filters.subject type="subject"}} -
- - {{#load-more selector=".group-logs .group-logs-row" action="loadMore"}} - - - - - - - - - - - - {{#each model.logs as |log|}} - {{group-logs-row - log=log - filters=filters}} - {{/each}} - -
{{i18n 'groups.logs.action'}}{{i18n 'groups.logs.acting_user'}}{{i18n 'groups.logs.target_user'}}{{i18n 'groups.logs.subject'}}{{i18n 'groups.logs.when'}}
- {{/load-more}} - - {{conditional-loading-spinner condition=loading}} -{{else}} -
{{i18n "groups.empty.logs"}}
-{{/if}} diff --git a/app/assets/javascripts/discourse/templates/group.hbs b/app/assets/javascripts/discourse/templates/group.hbs index 6fc009b830..dfb900b91e 100644 --- a/app/assets/javascripts/discourse/templates/group.hbs +++ b/app/assets/javascripts/discourse/templates/group.hbs @@ -42,12 +42,6 @@
{{group-navigation group=model currentPath=application.currentPath tabs=tabs}} - {{#if canManageGroup}} - {{group-navigation-dropdown - model=model - manageMembership=(route-action "showGroupMembershipModal")}} - {{/if}} - {{#if displayGroupMessageButton}} {{d-button action="messageGroup" diff --git a/app/assets/javascripts/discourse/templates/group/manage.hbs b/app/assets/javascripts/discourse/templates/group/manage.hbs new file mode 100644 index 0000000000..ed08fad7aa --- /dev/null +++ b/app/assets/javascripts/discourse/templates/group/manage.hbs @@ -0,0 +1,15 @@ +
+ {{#mobile-nav class='group-manage-nav' desktopClass='pull-left nav nav-stacked' currentPath=application.currentPath}} + {{#each tabs as |tab|}} +
  • + {{#link-to tab.route model.name}} + {{i18n tab.title}} + {{/link-to}} +
  • + {{/each}} + {{/mobile-nav}} + +
    + {{outlet}} +
    +
    diff --git a/app/assets/javascripts/discourse/templates/group/manage/logs.hbs b/app/assets/javascripts/discourse/templates/group/manage/logs.hbs new file mode 100644 index 0000000000..d21a3a98ae --- /dev/null +++ b/app/assets/javascripts/discourse/templates/group/manage/logs.hbs @@ -0,0 +1,33 @@ +{{#if model.logs}} +
    + {{group-manage-logs-filter clearFilter="clearFilter" value=filters.action type="action"}} + {{group-manage-logs-filter clearFilter="clearFilter" value=filters.acting_user type="acting_user"}} + {{group-manage-logs-filter clearFilter="clearFilter" value=filters.target_user type="target_user"}} + {{group-manage-logs-filter clearFilter="clearFilter" value=filters.subject type="subject"}} +
    + + {{#load-more selector=".group-manage-logs .group-manage-logs-row" action="loadMore"}} + + + + + + + + + + + + {{#each model.logs as |log|}} + {{group-manage-logs-row + log=log + filters=filters}} + {{/each}} + +
    {{i18n 'groups.manage.logs.action'}}{{i18n 'groups.manage.logs.acting_user'}}{{i18n 'groups.manage.logs.target_user'}}{{i18n 'groups.manage.logs.subject'}}{{i18n 'groups.manage.logs.when'}}
    + {{/load-more}} + + {{conditional-loading-spinner condition=loading}} +{{else}} +
    {{i18n "groups.empty.logs"}}
    +{{/if}} diff --git a/app/assets/javascripts/discourse/templates/group/manage/members.hbs b/app/assets/javascripts/discourse/templates/group/manage/members.hbs new file mode 100644 index 0000000000..3d1ea6cdd6 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/group/manage/members.hbs @@ -0,0 +1,25 @@ +
    +
    + + + {{user-selector + usernames=model.usernames + placeholderKey="groups.selector_placeholder" + id="group-manage-members-user-selector"}} +
    + + {{#if this.currentUser.admin}} +
    + +
    + {{/if}} + + {{d-button action="addMembers" + class="add btn-primary" + icon="plus" + disabled=disableAddButton + label="groups.add"}} +
    diff --git a/app/assets/javascripts/discourse/templates/group/manage/profile.hbs b/app/assets/javascripts/discourse/templates/group/manage/profile.hbs new file mode 100644 index 0000000000..d90ae631b6 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/group/manage/profile.hbs @@ -0,0 +1,71 @@ +
    + {{#if this.currentUser.admin}} +
    + + {{input type='text' name='name' value=model.name class='group-manage-name'}} +
    + {{/if}} + +
    + + {{input type='text' name='full_name' value=model.full_name class='group-manage-full-name'}} +
    + +
    + + {{d-editor value=model.bio_raw class="group-manage-bio"}} +
    + +
    + {{group-flair-inputs model=model}} +
    + +
    + +
    + +
    + +
    + +
    + +
    + + {{#if model.allow_membership_requests}} +
    + + + {{expanding-text-area name="membership-request-template" + value=model.membership_request_template + class="group-manage-membership-request-template"}} +
    + {{/if}} + + {{plugin-outlet name="group-manage" args=(hash group=model)}} + + {{d-button action="save" class="btn-primary" disabled=saving label="save"}} + {{savingText}} +
    diff --git a/app/assets/javascripts/discourse/templates/modal/group-membership.hbs b/app/assets/javascripts/discourse/templates/modal/group-membership.hbs deleted file mode 100644 index 92c35ab9f0..0000000000 --- a/app/assets/javascripts/discourse/templates/modal/group-membership.hbs +++ /dev/null @@ -1,25 +0,0 @@ -{{#d-modal-body class='group-membership' title="groups.add_members.title"}} -
    - - - {{user-selector - usernames=model.usernames - placeholderKey="groups.selector_placeholder" - id="group-membership-user-selector"}} -
    - - {{#if this.currentUser.admin}} -
    - - {{input type="checkbox" class="inline" checked=setAsOwner}} -
    - {{/if}} -{{/d-modal-body}} - - diff --git a/app/assets/stylesheets/common/base/group.scss b/app/assets/stylesheets/common/base/group.scss index 082adb401e..fbcfd1d489 100644 --- a/app/assets/stylesheets/common/base/group.scss +++ b/app/assets/stylesheets/common/base/group.scss @@ -78,7 +78,7 @@ } } -.group-logs-filter { +.group-manage-logs-filter { margin-right: 10px; &:hover { @@ -86,7 +86,7 @@ } } -table.group-logs { +table.group-manage-logs { width: 100%; th, tr { @@ -102,7 +102,7 @@ table.group-logs { padding: 10px 0; } - .group-logs-expand-details { + .group-manage-logs-expand-details { cursor: pointer; i { @@ -201,7 +201,7 @@ table.group-members { } } -.group-edit { +.group-manage { .form-horizontal { label { font-weight: bold; @@ -209,16 +209,8 @@ table.group-members { } } -.group-membership { - .ac-wrap { - width: 100% !important; - } - - label { - font-weight: bold; - } - - .group-membership-make-owner { +.group-manage-members { + .group-manage-members-make-owner { label { display: inline; vertical-align: middle; diff --git a/app/assets/stylesheets/common/base/request-group-membership-form.scss b/app/assets/stylesheets/common/base/request-group-membership-form.scss index 63dfd0aec7..19b2213c82 100644 --- a/app/assets/stylesheets/common/base/request-group-membership-form.scss +++ b/app/assets/stylesheets/common/base/request-group-membership-form.scss @@ -4,6 +4,6 @@ } } -.group-edit-membership-request-template { +.group-manage-membership-request-template { width: 98%; } diff --git a/app/assets/stylesheets/desktop/group.scss b/app/assets/stylesheets/desktop/group.scss index e279256e5c..d34fb0b0a5 100644 --- a/app/assets/stylesheets/desktop/group.scss +++ b/app/assets/stylesheets/desktop/group.scss @@ -20,7 +20,10 @@ margin-bottom: 20px; } -.group-activity-nav, .group-messages-nav { +.group-activity-nav, +.group-messages-nav, +.group-manage-nav +{ width: 15%; background-color: transparent; @@ -43,14 +46,14 @@ } } -.group-activity-outlet, .group-messages-outlet { +.group-activity-outlet, +.group-messages-outlet, +.group-manage-outlet +{ width: 85%; } -.group-edit { - border: 1px solid $primary-low; - padding: 10px; - +.group-manage { .form-horizontal { button { float: none; diff --git a/app/assets/stylesheets/mobile/group.scss b/app/assets/stylesheets/mobile/group.scss index 0ab1c39142..226f58d339 100644 --- a/app/assets/stylesheets/mobile/group.scss +++ b/app/assets/stylesheets/mobile/group.scss @@ -23,11 +23,14 @@ float: left; } -.group-activity { +.group-activity, .group-manage { position: relative; } -.group-activity-nav, .group-messages-nav { +.group-activity-nav, +.group-messages-nav, +.group-manage-nav +{ &.mobile-nav { position: absolute; right: 0; @@ -51,7 +54,7 @@ } } -table.group-logs { +table.group-manage-logs { width: 130%; } diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index b41f170edf..a30a34b8f1 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -411,7 +411,7 @@ class GroupsController < ApplicationController private def group_params - params.require(:group).permit( + permitted_params = [ :flair_url, :flair_bg_color, :flair_color, @@ -421,7 +421,13 @@ class GroupsController < ApplicationController :public_exit, :allow_membership_requests, :membership_request_template, - ) + ] + + if current_user.admin + permitted_params.push(:name) + end + + params.require(:group).permit(*permitted_params) end def find_group(param_name) diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index d3144ca4c2..31f073ba17 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -403,21 +403,29 @@ en: remove_user_as_group_owner: "Revoke owner" groups: - logs: - title: "Logs" - when: "When" - action: "Action" - acting_user: "Acting user" - target_user: "Target user" - subject: "Subject" - details: "Details" - from: "From" - to: "To" - edit: - title: 'Edit Group' + manage: + title: 'Manage' + name: 'Name' full_name: 'Full Name' add_members: "Add Members" delete_member_confirm: "Remove '%{username}' from the '%{group}' group?" + profile: + title: Profile + members: + title: "Members" + description: "Manage the membership of this group" + usernames: "Usernames" + as_owner: "Set user(s) as owner(s) of this group" + logs: + title: "Logs" + when: "When" + action: "Action" + acting_user: "Acting user" + target_user: "Target user" + subject: "Subject" + details: "Details" + from: "From" + to: "To" name_placeholder: "Group name, no spaces, same as username rule" public_admission: "Allow users to join the group freely (Requires publicly visible group)" public_exit: "Allow users to leave the group freely" @@ -464,11 +472,6 @@ en: one: "Group" other: "Groups" activity: "Activity" - add_members: - title: "Add Members" - description: "Manage the membership of this group" - usernames: "Usernames" - as_owner: "Set user(s) as owner(s) of this group" members: title: "Members" filter_placeholder_admin: "username or email" diff --git a/config/routes.rb b/config/routes.rb index a5249b598d..38cf78b4e5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -472,6 +472,10 @@ Discourse::Application.routes.draw do messages messages/inbox messages/archive + manage + manage/profile + manage/members + manage/logs }.each do |path| get path => 'groups#show' end diff --git a/spec/requests/groups_controller_spec.rb b/spec/requests/groups_controller_spec.rb index f859a8058d..9532b92777 100644 --- a/spec/requests/groups_controller_spec.rb +++ b/spec/requests/groups_controller_spec.rb @@ -279,6 +279,7 @@ describe GroupsController do expect do put "/groups/#{group.id}.json", params: { group: { + name: 'testing', flair_bg_color: 'FFF', flair_color: 'BBB', flair_url: 'fa-adjust', @@ -292,7 +293,7 @@ describe GroupsController do } end.to change { GroupHistory.count }.by(9) - expect(response).to be_success + expect(response.status).to eq(200) group.reload @@ -306,6 +307,7 @@ describe GroupsController do expect(group.allow_membership_requests).to eq(true) expect(group.membership_request_template).to eq('testing') expect(GroupHistory.last.subject).to eq('membership_request_template') + expect(group.name).to eq('test') end end @@ -316,10 +318,18 @@ describe GroupsController do end it 'should be able to update the group' do - put "/groups/#{group.id}.json", params: { group: { flair_color: 'BBB' } } + put "/groups/#{group.id}.json", params: { + group: { + flair_color: 'BBB', + name: 'testing' + } + } - expect(response).to be_success - expect(group.reload.flair_color).to eq('BBB') + expect(response.status).to eq(200) + + group.reload + expect(group.flair_color).to eq('BBB') + expect(group.name).to eq('testing') end end diff --git a/test/javascripts/acceptance/group-edit-test.js.es6 b/test/javascripts/acceptance/group-edit-test.js.es6 deleted file mode 100644 index a03cb17629..0000000000 --- a/test/javascripts/acceptance/group-edit-test.js.es6 +++ /dev/null @@ -1,61 +0,0 @@ -import { acceptance, logIn } from "helpers/qunit-helpers"; - -acceptance("Editing Group"); - -QUnit.test("Editing group", assert => { - logIn(); - Discourse.reset(); - - visit("/groups/discourse/edit"); - - andThen(() => { - assert.ok(find('.group-flair-inputs').length === 1, 'it should display avatar flair inputs'); - assert.ok(find('.group-edit-bio').length === 1, 'it should display group bio input'); - assert.ok(find('.group-edit-full-name').length === 1, 'it should display group full name input'); - - assert.ok( - find('.group-edit-public-admission').length === 1, - 'it should display group public admission input' - ); - - assert.ok( - find('.group-edit-public-exit').length === 1, - 'it should display group public exit input' - ); - - assert.ok(find('.group-edit-allow-membership-requests').length === 1, 'it should display group allow_membership_requets input'); - - assert.ok( - find('.group-edit-allow-membership-requests[disabled]').length === 1, - 'it should disable group allow_membership_request input' - ); - }); - - click('.group-edit-public-admission'); - click('.group-edit-allow-membership-requests'); - - andThen(() => { - assert.ok( - find('.group-edit-public-admission[disabled]').length === 1, - 'it should disable group public admission input' - ); - - assert.ok( - find('.group-edit-public-exit[disabled]').length === 0, - 'it should not disable group public exit input' - ); - - assert.equal( - find('.group-edit-membership-request-template').length, 1, - 'it should display the membership request template field' - ); - }); -}); - -QUnit.test("Editing group as an anonymous user", assert => { - visit("/groups/discourse/edit"); - - andThen(() => { - assert.ok(count('.group-members tr') > 0, "it should redirect to members page for an anonymous user"); - }); -}); diff --git a/test/javascripts/acceptance/group-index-test.js.es6 b/test/javascripts/acceptance/group-index-test.js.es6 index cfd52691af..caa0a3b8d9 100644 --- a/test/javascripts/acceptance/group-index-test.js.es6 +++ b/test/javascripts/acceptance/group-index-test.js.es6 @@ -9,11 +9,6 @@ QUnit.test("Viewing Members as anon user", assert => { assert.ok(count('.avatar-flair .fa-adjust') === 1, "it displays the group's avatar flair"); assert.ok(count('.group-members tr') > 0, "it lists group members"); - assert.ok( - count('.group-navigation-dropdown') === 0, - 'it should not display the group navigation dropdown menu' - ); - assert.ok( count('.group-member-dropdown') === 0, 'it does not allow anon user to manage group members' @@ -34,11 +29,6 @@ QUnit.test("Viewing Members as an admin user", assert => { visit("/groups/discourse"); andThen(() => { - assert.ok( - count('.group-navigation-dropdown') === 1, - 'it should display the group navigation dropdown menu' - ); - assert.ok( count('.group-member-dropdown') > 0, 'it allows admin user to manage group members' @@ -50,23 +40,4 @@ QUnit.test("Viewing Members as an admin user", assert => { 'it should display the right filter placehodler' ); }); - - selectKit('.group-navigation-dropdown').expand().selectRowByValue('manageMembership'); - - andThen(() => { - assert.ok( - count('.group-membership') === 1, - 'it should display the right modal' - ); - - assert.ok( - count('#group-membership-user-selector') === 1, - 'it should display the user selector' - ); - - assert.ok( - count(".group-membership-make-owner input[type='checkbox']") === 1, - 'it should display the input to set users as owners' - ); - }); }); diff --git a/test/javascripts/acceptance/group-logs-test.js.es6 b/test/javascripts/acceptance/group-manage-logs-test.js.es6 similarity index 85% rename from test/javascripts/acceptance/group-logs-test.js.es6 rename to test/javascripts/acceptance/group-manage-logs-test.js.es6 index 86d760cb0c..285ce77899 100644 --- a/test/javascripts/acceptance/group-logs-test.js.es6 +++ b/test/javascripts/acceptance/group-manage-logs-test.js.es6 @@ -1,6 +1,6 @@ import { acceptance } from "helpers/qunit-helpers"; -acceptance("Group Logs", { +acceptance("Group logs", { loggedIn: true, beforeEach() { const response = object => { @@ -27,14 +27,14 @@ acceptance("Group Logs", { }); QUnit.test("Browsing group logs", assert => { - visit("/groups/snorlax/logs"); + visit("/groups/snorlax/manage/logs"); andThen(() => { - assert.ok(find('tr.group-logs-row').length === 2, 'it should display the right number of logs'); - click(find(".group-logs-row button")[0]); + assert.ok(find('tr.group-manage-logs-row').length === 2, 'it should display the right number of logs'); + click(find(".group-manage-logs-row button")[0]); }); andThen(() => { - assert.ok(find('tr.group-logs-row').length === 1, 'it should display the right number of logs'); + assert.ok(find('tr.group-manage-logs-row').length === 1, 'it should display the right number of logs'); }); -}); \ No newline at end of file +}); diff --git a/test/javascripts/acceptance/group-manage-profile-test.js.es6 b/test/javascripts/acceptance/group-manage-profile-test.js.es6 new file mode 100644 index 0000000000..fd801b4aed --- /dev/null +++ b/test/javascripts/acceptance/group-manage-profile-test.js.es6 @@ -0,0 +1,62 @@ +import { acceptance, logIn } from "helpers/qunit-helpers"; + +acceptance("Managing Group Profile"); + +QUnit.test("Editing group", assert => { + logIn(); + Discourse.reset(); + + visit("/groups/discourse/manage/profile"); + + andThen(() => { + assert.ok(find('.group-flair-inputs').length === 1, 'it should display avatar flair inputs'); + assert.ok(find('.group-manage-bio').length === 1, 'it should display group bio input'); + assert.ok(find('.group-manage-name').length === 1, 'it should display group name input'); + assert.ok(find('.group-manage-full-name').length === 1, 'it should display group full name input'); + + assert.ok( + find('.group-manage-public-admission').length === 1, + 'it should display group public admission input' + ); + + assert.ok( + find('.group-manage-public-exit').length === 1, + 'it should display group public exit input' + ); + + assert.ok(find('.group-manage-allow-membership-requests').length === 1, 'it should display group allow_membership_requets input'); + + assert.ok( + find('.group-manage-allow-membership-requests[disabled]').length === 1, + 'it should disable group allow_membership_request input' + ); + }); + + click('.group-manage-public-admission'); + click('.group-manage-allow-membership-requests'); + + andThen(() => { + assert.ok( + find('.group-manage-public-admission[disabled]').length === 1, + 'it should disable group public admission input' + ); + + assert.ok( + find('.group-manage-public-exit[disabled]').length === 0, + 'it should not disable group public exit input' + ); + + assert.equal( + find('.group-manage-membership-request-template').length, 1, + 'it should display the membership request template field' + ); + }); +}); + +QUnit.test("Editing group as an anonymous user", assert => { + visit("/groups/discourse/manage/profile"); + + andThen(() => { + assert.ok(count('.group-members tr') > 0, "it should redirect to members page for an anonymous user"); + }); +}); diff --git a/test/javascripts/acceptance/groups-test.js.es6 b/test/javascripts/acceptance/groups-test.js.es6 index 0c3e94dded..78f5526841 100644 --- a/test/javascripts/acceptance/groups-test.js.es6 +++ b/test/javascripts/acceptance/groups-test.js.es6 @@ -193,8 +193,11 @@ QUnit.test("Admin Viewing Group", assert => { visit("/groups/discourse"); andThen(() => { - assert.ok(find(".nav-pills li a[title='Edit Group']").length === 1, 'it should show edit group tab if user is admin'); - assert.ok(find(".nav-pills li a[title='Logs']").length === 1, 'it should show Logs tab if user is admin'); + assert.ok( + find(".nav-pills li a[title='Manage']").length === 1, + 'it should show manage group tab if user is admin' + ); + assert.equal(count('.group-message-button'), 1, 'it displays show group message button'); assert.equal(find('.group-info-name').text(), 'Awesome Team', 'it should display the group name'); }); From 68ae009f98afd1284121d8195b2851195b72fa56 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 27 Mar 2018 13:52:28 +0800 Subject: [PATCH 016/287] Update group navigation link style. --- .../discourse/templates/group/activity.hbs | 2 +- .../discourse/templates/group/manage.hbs | 2 +- .../discourse/templates/group/messages.hbs | 3 ++- app/assets/stylesheets/desktop/group.scss | 22 +++++++++---------- app/assets/stylesheets/mobile/group.scss | 5 +---- 5 files changed, 15 insertions(+), 19 deletions(-) diff --git a/app/assets/javascripts/discourse/templates/group/activity.hbs b/app/assets/javascripts/discourse/templates/group/activity.hbs index 957c08e939..06dd803657 100644 --- a/app/assets/javascripts/discourse/templates/group/activity.hbs +++ b/app/assets/javascripts/discourse/templates/group/activity.hbs @@ -1,5 +1,5 @@
    - {{#mobile-nav class='group-activity-nav' desktopClass="pull-left nav nav-stacked" currentPath=application.currentPath}} + {{#mobile-nav class='group-activity-nav group-navigation' desktopClass="pull-left nav nav-stacked" currentPath=application.currentPath}} {{group-activity-filter filter="posts" categoryId=category_id}} {{group-activity-filter filter="topics" categoryId=category_id}} {{#if siteSettings.enable_mentions}} diff --git a/app/assets/javascripts/discourse/templates/group/manage.hbs b/app/assets/javascripts/discourse/templates/group/manage.hbs index ed08fad7aa..0f1e4c9f88 100644 --- a/app/assets/javascripts/discourse/templates/group/manage.hbs +++ b/app/assets/javascripts/discourse/templates/group/manage.hbs @@ -1,5 +1,5 @@
    - {{#mobile-nav class='group-manage-nav' desktopClass='pull-left nav nav-stacked' currentPath=application.currentPath}} + {{#mobile-nav class='group-navigation' desktopClass='pull-left nav nav-stacked' currentPath=application.currentPath}} {{#each tabs as |tab|}}
  • {{#link-to tab.route model.name}} diff --git a/app/assets/javascripts/discourse/templates/group/messages.hbs b/app/assets/javascripts/discourse/templates/group/messages.hbs index 5d86ab3df4..923a6017d0 100644 --- a/app/assets/javascripts/discourse/templates/group/messages.hbs +++ b/app/assets/javascripts/discourse/templates/group/messages.hbs @@ -1,5 +1,6 @@
    - {{#mobile-nav class='group-messages-nav' desktopClass='pull-left nav nav-stacked' currentPath=application.currentPath}} + {{#mobile-nav class='group-navigation' desktopClass='pull-left nav nav-stacked' currentPath=application.currentPath}} +
  • {{#link-to 'group.messages.inbox' model.name}} {{i18n 'user.messages.inbox'}} diff --git a/app/assets/stylesheets/desktop/group.scss b/app/assets/stylesheets/desktop/group.scss index d34fb0b0a5..651d154d5a 100644 --- a/app/assets/stylesheets/desktop/group.scss +++ b/app/assets/stylesheets/desktop/group.scss @@ -20,10 +20,7 @@ margin-bottom: 20px; } -.group-activity-nav, -.group-messages-nav, -.group-manage-nav -{ +.group-navigation { width: 15%; background-color: transparent; @@ -31,17 +28,18 @@ border: none; a { + color: dark-light-choose($primary-medium, $secondary-high); padding: 8px 13px; - } - a.active { - background-color: transparent; - font-weight: bold; - color: $primary; - } + &.active { + background-color: transparent; + font-weight: bold; + color: $primary; - a.active:after { - display: none; + &:after { + display: none; + } + } } } } diff --git a/app/assets/stylesheets/mobile/group.scss b/app/assets/stylesheets/mobile/group.scss index 226f58d339..c7e3670e6e 100644 --- a/app/assets/stylesheets/mobile/group.scss +++ b/app/assets/stylesheets/mobile/group.scss @@ -27,10 +27,7 @@ position: relative; } -.group-activity-nav, -.group-messages-nav, -.group-manage-nav -{ +.group-navigation { &.mobile-nav { position: absolute; right: 0; From e7407d0adc7a3d9cace7fe464a0aea75a5b3d816 Mon Sep 17 00:00:00 2001 From: Vinoth Kannan Date: Tue, 27 Mar 2018 11:53:35 +0530 Subject: [PATCH 017/287] FEATURE: Webhook for group and category events --- app/controllers/admin/groups_controller.rb | 5 ++- app/controllers/categories_controller.rb | 2 ++ app/controllers/groups_controller.rb | 1 + app/models/category.rb | 13 ++++++++ app/models/group.rb | 13 ++++++++ app/models/web_hook_event_type.rb | 2 ++ config/initializers/012-web_hook_events.rb | 20 ++++++++++++ config/locales/client.en.yml | 6 ++++ db/fixtures/007_web_hook_event_types.rb | 10 ++++++ spec/models/category_spec.rb | 20 +++++++++++- spec/models/group_spec.rb | 36 ++++++++++++++++----- spec/requests/categories_controller_spec.rb | 24 ++++++++++++++ spec/requests/groups_controller_spec.rb | 9 ++++++ 13 files changed, 151 insertions(+), 10 deletions(-) create mode 100644 spec/requests/categories_controller_spec.rb diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb index 8a6a7a7239..cb784c14d1 100644 --- a/app/controllers/admin/groups_controller.rb +++ b/app/controllers/admin/groups_controller.rb @@ -55,7 +55,10 @@ class Admin::GroupsController < Admin::AdminController # group rename is ignored for automatic groups group.name = group_params[:name] if group_params[:name] && !group.automatic - save_group(group) { |g| GroupActionLogger.new(current_user, g).log_change_group_settings } + save_group(group) do |group| + GroupActionLogger.new(current_user, group).log_change_group_settings + DiscourseEvent.trigger(:group_updated, group) + end end def save_group(group) diff --git a/app/controllers/categories_controller.rb b/app/controllers/categories_controller.rb index aaaf1ec7bd..e2fa98fda1 100644 --- a/app/controllers/categories_controller.rb +++ b/app/controllers/categories_controller.rb @@ -149,6 +149,7 @@ class CategoriesController < ApplicationController old_permissions = cat.permissions_params if result = cat.update(category_params) + DiscourseEvent.trigger(:category_updated, cat) Scheduler::Defer.later "Log staff action change category settings" do @staff_action_logger.log_category_settings_change(@category, category_params, old_permissions) end @@ -165,6 +166,7 @@ class CategoriesController < ApplicationController custom_slug = params[:slug].to_s if custom_slug.present? && @category.update_attributes(slug: custom_slug) + DiscourseEvent.trigger(:category_updated, @category) render json: success_json else render_json_error(@category) diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 70d219afc0..3f5ac6bbe8 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -122,6 +122,7 @@ class GroupsController < ApplicationController if group.update_attributes(group_params) GroupActionLogger.new(current_user, group).log_change_group_settings + DiscourseEvent.trigger(:group_updated, group) render json: success_json else diff --git a/app/models/category.rb b/app/models/category.rb index e551227ed6..130eb6ff12 100644 --- a/app/models/category.rb +++ b/app/models/category.rb @@ -61,6 +61,9 @@ class Category < ActiveRecord::Base after_update :rename_category_definition, if: :saved_change_to_name? after_update :create_category_permalink, if: :saved_change_to_slug? + after_commit :trigger_category_created_event, on: :create + after_commit :trigger_category_destroyed_event, on: :destroy + belongs_to :parent_category, class_name: 'Category' has_many :subcategories, class_name: 'Category', foreign_key: 'parent_category_id' @@ -511,6 +514,16 @@ SQL def subcategory_list_includes_topics? subcategory_list_style.end_with?("with_featured_topics") end + + def trigger_category_created_event + DiscourseEvent.trigger(:category_created, self) + true + end + + def trigger_category_destroyed_event + DiscourseEvent.trigger(:category_destroyed, self) + true + end end # == Schema Information diff --git a/app/models/group.rb b/app/models/group.rb index 90bb845f63..c3e1399227 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -35,6 +35,9 @@ class Group < ActiveRecord::Base after_save :expire_cache after_destroy :expire_cache + after_commit :trigger_group_created_event, on: :create + after_commit :trigger_group_destroyed_event, on: :destroy + def expire_cache ApplicationSerializer.expire_cache_fragment!("group_names") end @@ -571,6 +574,16 @@ class Group < ActiveRecord::Base self.member_of(groups, user).where("gu.owner") end + def trigger_group_created_event + DiscourseEvent.trigger(:group_created, self) + true + end + + def trigger_group_destroyed_event + DiscourseEvent.trigger(:group_destroyed, self) + true + end + protected def name_format_validator diff --git a/app/models/web_hook_event_type.rb b/app/models/web_hook_event_type.rb index d3596bfdaf..a98bd5a99d 100644 --- a/app/models/web_hook_event_type.rb +++ b/app/models/web_hook_event_type.rb @@ -2,6 +2,8 @@ class WebHookEventType < ActiveRecord::Base TOPIC = 1 POST = 2 USER = 3 + GROUP = 4 + CATEGORY = 5 has_and_belongs_to_many :web_hooks diff --git a/config/initializers/012-web_hook_events.rb b/config/initializers/012-web_hook_events.rb index 7e29c2d301..158d09e3f0 100644 --- a/config/initializers/012-web_hook_events.rb +++ b/config/initializers/012-web_hook_events.rb @@ -42,3 +42,23 @@ end WebHook.enqueue_hooks(:user, user_id: user.id, event_name: event.to_s) end end + +%i( + group_created + group_updated + group_destroyed +).each do |event| + DiscourseEvent.on(event) do |group| + WebHook.enqueue_hooks(:group, group_id: group.id, event_name: event.to_s) + end +end + +%i( + category_created + category_updated + category_destroyed +).each do |event| + DiscourseEvent.on(event) do |category| + WebHook.enqueue_hooks(:category, category_id: category.id, event_name: event.to_s) + end +end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 8b4f2ff813..2cfd5e1a1e 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -2915,6 +2915,12 @@ en: user_event: name: "User Event" details: "When a user logs in, logs out, is created, approved or updated." + group_event: + name: "Group Event" + details: "When a group is created, updated or destroyed." + category_event: + name: "Category Event" + details: "When a category is created, updated or destroyed." delivery_status: title: "Delivery Status" inactive: "Inactive" diff --git a/db/fixtures/007_web_hook_event_types.rb b/db/fixtures/007_web_hook_event_types.rb index d91b5f09a1..c3f8b8b150 100644 --- a/db/fixtures/007_web_hook_event_types.rb +++ b/db/fixtures/007_web_hook_event_types.rb @@ -12,3 +12,13 @@ WebHookEventType.seed do |b| b.id = WebHookEventType::USER b.name = "user" end + +WebHookEventType.seed do |b| + b.id = WebHookEventType::GROUP + b.name = "group" +end + +WebHookEventType.seed do |b| + b.id = WebHookEventType::CATEGORY + b.name = "category" +end diff --git a/spec/models/category_spec.rb b/spec/models/category_spec.rb index 9f756f394f..51992b1ce5 100644 --- a/spec/models/category_spec.rb +++ b/spec/models/category_spec.rb @@ -368,6 +368,17 @@ describe Category do end end + describe 'new' do + subject { Fabricate.build(:category, user: Fabricate(:user)) } + + it 'triggers a extensibility event' do + event = DiscourseEvent.track_events { subject.save! }.last + + expect(event[:event_name]).to eq(:category_created) + expect(event[:params].first).to eq(subject) + end + end + describe "update" do it "should enforce uniqueness of slug" do Fabricate(:category, slug: "the-slug") @@ -384,14 +395,21 @@ describe Category do @category_id = @category.id @topic_id = @category.topic_id SiteSetting.shared_drafts_category = @category.id.to_s - @category.destroy end it 'is deleted correctly' do + @category.destroy expect(Category.exists?(id: @category_id)).to be false expect(Topic.exists?(id: @topic_id)).to be false expect(SiteSetting.shared_drafts_category).to be_blank end + + it 'triggers a extensibility event' do + event = DiscourseEvent.track_events { @category.destroy }.first + + expect(event[:event_name]).to eq(:category_destroyed) + expect(event[:params].first).to eq(@category) + end end describe 'latest' do diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 20b1e242d1..255b1d245e 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -411,17 +411,37 @@ describe Group do expect(g.usernames.split(",").sort).to eq usernames.split(",").sort end - it "correctly destroys groups" do + describe 'new' do + subject { Fabricate.build(:group) } - g = Fabricate(:group) - u1 = Fabricate(:user) - g.add(u1) - g.save! + it 'triggers a extensibility event' do + event = DiscourseEvent.track_events { subject.save! }.first - g.destroy + expect(event[:event_name]).to eq(:group_created) + expect(event[:params].first).to eq(subject) + end + end - expect(User.where(id: u1.id).count).to eq 1 - expect(GroupUser.where(group_id: g.id).count).to eq 0 + describe 'destroy' do + let(:user) { Fabricate(:user) } + let(:group) { Fabricate(:group, users: [user]) } + + before do + group.add(user) + end + + it "it deleted correctly" do + group.destroy! + expect(User.where(id: user.id).count).to eq 1 + expect(GroupUser.where(group_id: group.id).count).to eq 0 + end + + it 'triggers a extensibility event' do + event = DiscourseEvent.track_events { group.destroy! }.first + + expect(event[:event_name]).to eq(:group_destroyed) + expect(event[:params].first).to eq(group) + end end it "has custom fields" do diff --git a/spec/requests/categories_controller_spec.rb b/spec/requests/categories_controller_spec.rb new file mode 100644 index 0000000000..9934db2450 --- /dev/null +++ b/spec/requests/categories_controller_spec.rb @@ -0,0 +1,24 @@ +require 'rails_helper' + +describe GroupsController do + let(:admin) { Fabricate(:admin) } + let(:category) { Fabricate(:category, user: admin) } + + before do + category + sign_in(admin) + end + + it "triggers a extensibility event" do + event = DiscourseEvent.track_events { + put "/categories/#{category.id}.json", params: { + name: 'hello', + color: 'ff0', + text_color: 'fff' + } + }.last + + expect(event[:event_name]).to eq(:category_updated) + expect(event[:params].first).to eq(category) + end +end diff --git a/spec/requests/groups_controller_spec.rb b/spec/requests/groups_controller_spec.rb index 494d149989..57f12c1e57 100644 --- a/spec/requests/groups_controller_spec.rb +++ b/spec/requests/groups_controller_spec.rb @@ -321,6 +321,15 @@ describe GroupsController do expect(response).to be_success expect(group.reload.flair_color).to eq('BBB') end + + it 'triggers a extensibility event' do + event = DiscourseEvent.track_events { + put "/groups/#{group.id}.json", params: { group: { flair_color: 'BBB' } } + }.last + + expect(event[:event_name]).to eq(:group_updated) + expect(event[:params].first).to eq(group) + end end context "when user is not a group owner or admin" do From 6c70925c6f60e171c6221f0a54ecfd9ba459d21c Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 27 Mar 2018 17:36:13 +1100 Subject: [PATCH 018/287] PERF: add missing index for akismet Note, current practice if for plugins to submit PRs to core for any migrations required for plugins, so we can better control schema. Especially if core tables are being touched. In this case index has close to zero cost unless akismet is installed This reduces the akismet admin query from 20ms on every new page load to 0.5ms --- ...327062911_add_post_custom_fields_akismet_index.rb | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 db/migrate/20180327062911_add_post_custom_fields_akismet_index.rb diff --git a/db/migrate/20180327062911_add_post_custom_fields_akismet_index.rb b/db/migrate/20180327062911_add_post_custom_fields_akismet_index.rb new file mode 100644 index 0000000000..66fb4a227c --- /dev/null +++ b/db/migrate/20180327062911_add_post_custom_fields_akismet_index.rb @@ -0,0 +1,12 @@ +# This is our current pattern for data migrations needed by plugins, we prefer to keep them in core +# so schema is tightly controlled, especially if we are amending tables owned by core +# +# this index makes looking up posts requiring review much faster (20ms on meta) + +class AddPostCustomFieldsAkismetIndex < ActiveRecord::Migration[5.1] + def change + add_index :post_custom_fields, [:post_id], + name: 'idx_post_custom_fields_akismet', + where: "name = 'AKISMET_STATE' AND value = 'needs_review'" + end +end From 31dea5d5fc078cbdcb3a72658362e4fc10d22524 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 27 Mar 2018 17:57:19 +1100 Subject: [PATCH 019/287] correct flaky spec --- spec/components/middleware/request_tracker_spec.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/spec/components/middleware/request_tracker_spec.rb b/spec/components/middleware/request_tracker_spec.rb index bc7b1d608e..bd47b965ef 100644 --- a/spec/components/middleware/request_tracker_spec.rb +++ b/spec/components/middleware/request_tracker_spec.rb @@ -235,7 +235,7 @@ describe Middleware::RequestTracker do def app(result, sql_calls: 0, redis_calls: 0) lambda do |env| sql_calls.times do - User.where(id: -100).first + User.where(id: -100).pluck(:id) end redis_calls.times do $redis.get("x") @@ -260,6 +260,10 @@ describe Middleware::RequestTracker do end it "can correctly log detailed data" do + + # ensure pg is warmed up with the select 1 query + User.where(id: -100).pluck(:id) + tracker = Middleware::RequestTracker.new(app([200, {}, []], sql_calls: 2, redis_calls: 2)) tracker.call(env) From 558914b98672e7800aba94d7095b1f71fb14f658 Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Tue, 27 Mar 2018 11:14:06 +0200 Subject: [PATCH 020/287] Fix random spec errors --- app/controllers/groups_controller.rb | 2 +- spec/requests/groups_controller_spec.rb | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index a30a34b8f1..1cf32c8ead 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -253,7 +253,7 @@ class GroupsController < ApplicationController if (usernames = group.users.where(id: users.pluck(:id)).pluck(:username)).present? render_json_error(I18n.t( "groups.errors.member_already_exist", - username: usernames.join(", "), + username: usernames.sort.join(", "), count: usernames.size )) else diff --git a/spec/requests/groups_controller_spec.rb b/spec/requests/groups_controller_spec.rb index 9532b92777..5e47f4d95c 100644 --- a/spec/requests/groups_controller_spec.rb +++ b/spec/requests/groups_controller_spec.rb @@ -589,7 +589,8 @@ describe GroupsController do end it 'fails when multiple member already exists' do - user3 = Fabricate(:user) + user2.update!(username: 'alice') + user3 = Fabricate(:user, username: 'bob') [user2, user3].each { |user| group.add(user) } expect do @@ -601,7 +602,7 @@ describe GroupsController do expect(JSON.parse(response.body)["errors"]).to include(I18n.t( "groups.errors.member_already_exist", - username: "#{user2.username}, #{user3.username}", + username: "alice, bob", count: 2 )) end From 7edab1c0b9b72e3d04f4640302fac28edae073b4 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 27 Mar 2018 16:45:21 +0800 Subject: [PATCH 021/287] UX: Add `groups/custom/new` route for admins to create a new group. --- .../{groups.js.es6 => groups-index.js.es6} | 4 + .../discourse/controllers/groups-new.js.es6 | 103 ++++++++++ .../discourse/routes/app-route-map.js.es6 | 4 +- .../{groups.js.es6 => groups-index.js.es6} | 0 .../discourse/routes/groups-new.js.es6 | 21 ++ .../{groups.hbs => groups/index.hbs} | 6 + .../discourse/templates/groups/new.hbs | 183 ++++++++++++++++++ .../components/group-admin-dropdown.js.es6 | 29 +++ app/assets/stylesheets/common/base/group.scss | 3 +- app/assets/stylesheets/desktop.scss | 1 + app/assets/stylesheets/desktop/groups.scss | 19 ++ app/controllers/groups_controller.rb | 6 +- config/locales/client.en.yml | 12 +- config/routes.rb | 1 + spec/requests/groups_controller_spec.rb | 30 +++ .../{groups-test.js.es6 => group-test.js.es6} | 59 +----- .../acceptance/groups-index-test.js.es6 | 43 ++++ .../acceptance/groups-new-test.js.es6 | 72 +++++++ 18 files changed, 535 insertions(+), 61 deletions(-) rename app/assets/javascripts/discourse/controllers/{groups.js.es6 => groups-index.js.es6} (93%) create mode 100644 app/assets/javascripts/discourse/controllers/groups-new.js.es6 rename app/assets/javascripts/discourse/routes/{groups.js.es6 => groups-index.js.es6} (100%) create mode 100644 app/assets/javascripts/discourse/routes/groups-new.js.es6 rename app/assets/javascripts/discourse/templates/{groups.hbs => groups/index.hbs} (96%) create mode 100644 app/assets/javascripts/discourse/templates/groups/new.hbs create mode 100644 app/assets/javascripts/select-kit/components/group-admin-dropdown.js.es6 create mode 100644 app/assets/stylesheets/desktop/groups.scss rename test/javascripts/acceptance/{groups-test.js.es6 => group-test.js.es6} (65%) create mode 100644 test/javascripts/acceptance/groups-index-test.js.es6 create mode 100644 test/javascripts/acceptance/groups-new-test.js.es6 diff --git a/app/assets/javascripts/discourse/controllers/groups.js.es6 b/app/assets/javascripts/discourse/controllers/groups-index.js.es6 similarity index 93% rename from app/assets/javascripts/discourse/controllers/groups.js.es6 rename to app/assets/javascripts/discourse/controllers/groups-index.js.es6 index 006dd3baab..e8f4098ae8 100644 --- a/app/assets/javascripts/discourse/controllers/groups.js.es6 +++ b/app/assets/javascripts/discourse/controllers/groups-index.js.es6 @@ -35,6 +35,10 @@ export default Ember.Controller.extend({ actions: { loadMore() { this.get('model').loadMore(); + }, + + new() { + this.transitionToRoute("groups.new"); } } }); diff --git a/app/assets/javascripts/discourse/controllers/groups-new.js.es6 b/app/assets/javascripts/discourse/controllers/groups-new.js.es6 new file mode 100644 index 0000000000..00714cd8f2 --- /dev/null +++ b/app/assets/javascripts/discourse/controllers/groups-new.js.es6 @@ -0,0 +1,103 @@ +import { popupAjaxError } from 'discourse/lib/ajax-error'; +import computed from 'ember-addons/ember-computed-decorators'; +import User from "discourse/models/user"; +import InputValidation from 'discourse/models/input-validation'; +import debounce from 'discourse/lib/debounce'; + +export default Ember.Controller.extend({ + disableSave: null, + + aliasLevelOptions: [ + { name: I18n.t("groups.alias_levels.nobody"), value: 0 }, + { name: I18n.t("groups.alias_levels.mods_and_admins"), value: 2 }, + { name: I18n.t("groups.alias_levels.members_mods_and_admins"), value: 3 }, + { name: I18n.t("groups.alias_levels.everyone"), value: 99 } + ], + + visibilityLevelOptions: [ + { name: I18n.t("groups.visibility_levels.public"), value: 0 }, + { name: I18n.t("groups.visibility_levels.members"), value: 1 }, + { name: I18n.t("groups.visibility_levels.staff"), value: 2 }, + { name: I18n.t("groups.visibility_levels.owners"), value: 3 } + ], + + @computed('model.visibility_level', 'model.public_admission') + disableMembershipRequestSetting(visibility_level, publicAdmission) { + visibility_level = parseInt(visibility_level); + return (visibility_level !== 0) || publicAdmission; + }, + + @computed('basicNameValidation', 'uniqueNameValidation') + nameValidation(basicNameValidation, uniqueNameValidation) { + return uniqueNameValidation ? uniqueNameValidation : basicNameValidation; + }, + + @computed('model.name') + basicNameValidation(name) { + if (name === undefined) { + return this._failedInputValidation(); + }; + + if (name === "") { + this.set('uniqueNameValidation', null); + return this._failedInputValidation(I18n.t('groups.new.name.blank')); + } + + if (name.length < this.siteSettings.min_username_length) { + return this._failedInputValidation(I18n.t('groups.new.name.too_short')); + } + + if (name.length > this.siteSettings.max_username_length) { + return this._failedInputValidation(I18n.t('groups.new.name.too_long')); + } + + this.checkGroupName(); + + return this._failedInputValidation(I18n.t('groups.new.name.checking')); + }, + + checkGroupName: debounce(function() { + User.checkUsername(this.get('model.name')).then(response => { + const validationName = 'uniqueNameValidation'; + + if (response.available) { + this.set(validationName, InputValidation.create({ + ok: true, + reason: I18n.t('groups.new.name.available') + })); + + this.set('disableSave', false); + } else { + let reason; + + if (response.errors) { + reason = response.errors.join(' '); + } else { + reason = I18n.t('groups.new.name.not_available'); + } + + this.set(validationName, this._failedInputValidation(reason)); + } + }); + }, 500), + + _failedInputValidation(reason) { + this.set('disableSave', true); + + const options = { failed: true }; + if (reason) options.reason = reason; + return InputValidation.create(options); + }, + + actions: { + save() { + this.set('disableSave', true); + const group = this.get('model'); + + group.create().then(() => { + this.transitionToRoute("group.members", group.name); + }).catch(popupAjaxError) + .finally(() => this.set('disableSave', false)); + }, + } +}); diff --git a/app/assets/javascripts/discourse/routes/app-route-map.js.es6 b/app/assets/javascripts/discourse/routes/app-route-map.js.es6 index 524e851001..eece80d826 100644 --- a/app/assets/javascripts/discourse/routes/app-route-map.js.es6 +++ b/app/assets/javascripts/discourse/routes/app-route-map.js.es6 @@ -49,7 +49,9 @@ export default function() { this.route('categoryWithID', { path: '/c/:parentSlug/:slug/:id' }); }); - this.route('groups', { resetNamespace: true }); + this.route('groups', { resetNamespace: true }, function() { + this.route("new", { path: "custom/new" }); + }); this.route('group', { path: '/groups/:name', resetNamespace: true }, function() { this.route('members'); diff --git a/app/assets/javascripts/discourse/routes/groups.js.es6 b/app/assets/javascripts/discourse/routes/groups-index.js.es6 similarity index 100% rename from app/assets/javascripts/discourse/routes/groups.js.es6 rename to app/assets/javascripts/discourse/routes/groups-index.js.es6 diff --git a/app/assets/javascripts/discourse/routes/groups-new.js.es6 b/app/assets/javascripts/discourse/routes/groups-new.js.es6 new file mode 100644 index 0000000000..e7489737cb --- /dev/null +++ b/app/assets/javascripts/discourse/routes/groups-new.js.es6 @@ -0,0 +1,21 @@ +import Group from 'discourse/models/group'; + +export default Discourse.Route.extend({ + titleToken() { + return I18n.t('groups.new.title'); + }, + + model() { + return Group.create({ automatic: false, visibility_level: 0 }); + }, + + setupController(controller, model) { + controller.set("model", model); + }, + + afterModel() { + if (!(this.currentUser && this.currentUser.admin)) { + this.transitionTo("groups"); + } + }, +}); diff --git a/app/assets/javascripts/discourse/templates/groups.hbs b/app/assets/javascripts/discourse/templates/groups/index.hbs similarity index 96% rename from app/assets/javascripts/discourse/templates/groups.hbs rename to app/assets/javascripts/discourse/templates/groups/index.hbs index 2bfc5d1f5a..c86bc76ab8 100644 --- a/app/assets/javascripts/discourse/templates/groups.hbs +++ b/app/assets/javascripts/discourse/templates/groups/index.hbs @@ -1,6 +1,12 @@ {{#d-section pageClass="groups"}}

    {{i18n "groups.index.title"}}

    + {{#if currentUser.admin}} +
    + {{group-admin-dropdown new="new"}} +
    + {{/if}} +
    {{combo-box value=type content=types diff --git a/app/assets/javascripts/discourse/templates/groups/new.hbs b/app/assets/javascripts/discourse/templates/groups/new.hbs new file mode 100644 index 0000000000..51bca6eb78 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/groups/new.hbs @@ -0,0 +1,183 @@ +{{#d-section pageClass="groups-new"}} +

    {{i18n "groups.new.title"}}

    + +
    +
    + + + {{text-field name="name" + class="input-xxlarge" + value=model.name + placeholderKey="groups.name_placeholder"}} + + {{input-tip validation=nameValidation}} +
    + +
    + + + {{text-field name='full_name' + class="input-xxlarge group-manage-full-name" + value=model.full_name}} +
    + +
    + + + {{input value=model.title name="title" class="input-xxlarge"}} +
    + +
    + + {{d-editor value=model.bio_raw}} +
    + +
    + + + {{user-selector usernames=model.ownerUsernames + placeholderKey="groups.selector_placeholder" + id="owner-selector"}} +
    + +
    + + + {{user-selector usernames=model.usernames + placeholderKey="groups.selector_placeholder" + id="member-selector"}} +
    + +
    + + + {{combo-box name="alias" + valueAttribute="value" + value=model.visibility_level + content=visibilityLevelOptions + castInteger=true}} +
    + +
    + + + + + + + + + {{#if model.allow_membership_requests}} +
    + + + {{expanding-text-area name="membership-request-template" + value=model.membership_request_template}} +
    + {{/if}} +
    + +
    + + + {{combo-box name="alias" + valueAttribute="value" + value=model.mentionable_level + content=aliasLevelOptions}} +
    + +
    + + + {{combo-box name="alias" + valueAttribute="value" + value=model.messageable_level + content=aliasLevelOptions}} +
    + +
    + + + {{notifications-button i18nPrefix='groups.notifications' + value=model.default_notification_level}} +
    + +
    + + + {{list-setting name="automatic_membership" settingValue=model.emailDomains}} + + +
    + +
    + + + {{combo-box name="grant_trust_level" + valueAttribute="value" + value=model.grant_trust_level + content=trustLevelOptions}} +
    + + {{#if siteSettings.email_in}} +
    + + + {{text-field name="incoming_email" + class="input-xxlarge" + value=model.incoming_email + placeholderKey="admin.groups.incoming_email_placeholder"}} + + {{plugin-outlet name="group-email-in" args=(hash model=model)}} +
    + {{/if}} + +
    + {{group-flair-inputs model=model}} +
    + + {{plugin-outlet name="group-edit" args=(hash group=model)}} + +
    + {{d-button action="save" + disabled=disableSave + class='btn btn-primary' + label='groups.new.create'}} + + {{#link-to "groups"}} + {{i18n 'cancel'}} + {{/link-to}} +
    +
    +{{/d-section}} diff --git a/app/assets/javascripts/select-kit/components/group-admin-dropdown.js.es6 b/app/assets/javascripts/select-kit/components/group-admin-dropdown.js.es6 new file mode 100644 index 0000000000..67817ef81e --- /dev/null +++ b/app/assets/javascripts/select-kit/components/group-admin-dropdown.js.es6 @@ -0,0 +1,29 @@ +import DropdownSelectBoxComponent from "select-kit/components/dropdown-select-box"; + +export default DropdownSelectBoxComponent.extend({ + classNames: "groups-admin-dropdown pull-right", + headerIcon: ["bars", "caret-down"], + showFullTitle: false, + + computeContent() { + const items = [ + { + id: "new", + name: I18n.t("groups.new.title"), + description: I18n.t("groups.new.description"), + icon: "plus" + } + ]; + + return items; + }, + + mutateValue(value) { + switch (value) { + case 'new': { + this.sendAction("new"); + break; + } + } + }, +}); diff --git a/app/assets/stylesheets/common/base/group.scss b/app/assets/stylesheets/common/base/group.scss index fbcfd1d489..b0e90f9f0d 100644 --- a/app/assets/stylesheets/common/base/group.scss +++ b/app/assets/stylesheets/common/base/group.scss @@ -201,7 +201,8 @@ table.group-members { } } -.group-manage { +.group-manage, +.groups-new-page { .form-horizontal { label { font-weight: bold; diff --git a/app/assets/stylesheets/desktop.scss b/app/assets/stylesheets/desktop.scss index e11252ac35..b1a6d6b849 100644 --- a/app/assets/stylesheets/desktop.scss +++ b/app/assets/stylesheets/desktop.scss @@ -20,6 +20,7 @@ @import "desktop/history"; @import "desktop/queued-posts"; @import "desktop/group"; +@import "desktop/groups"; // Import all component-specific files @import "desktop/components/*"; diff --git a/app/assets/stylesheets/desktop/groups.scss b/app/assets/stylesheets/desktop/groups.scss new file mode 100644 index 0000000000..5c12eae292 --- /dev/null +++ b/app/assets/stylesheets/desktop/groups.scss @@ -0,0 +1,19 @@ +.groups-page { + .list-controls { + float: right; + } +} + +$filter-line-height: 1.5; + +.groups-filter { + .groups-type-filter { + .select-kit-header { + line-height: $filter-line-height; + } + } + + input { + line-height: $filter-line-height; + } +} diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 1cf32c8ead..a660d2a628 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -7,7 +7,8 @@ class GroupsController < ApplicationController :update, :histories, :request_membership, - :search + :search, + :new ] skip_before_action :preload_json, :check_xhr, only: [:posts_feed, :mentions_feed] @@ -113,6 +114,9 @@ class GroupsController < ApplicationController end end + def new + end + def edit end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 31f073ba17..b42d7e5bc0 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -403,6 +403,17 @@ en: remove_user_as_group_owner: "Revoke owner" groups: + new: + title: "New Group" + description: "Create a new group" + create: "Create" + name: + too_short: "Group name is too short" + too_long: "Group name is too long" + checking: "Checking group name availability..." + available: "Group name is available" + not_available: "Group name is not available" + blank: "Group name cannot be blank" manage: title: 'Manage' name: 'Name' @@ -452,7 +463,6 @@ en: submit: "Submit Request" title: "Request to join @%{group_name}" reason: "Let the group owners know why you belong in this group" - membership: "Membership" name: "Name" user_count: "Members Count" diff --git a/config/routes.rb b/config/routes.rb index 38cf78b4e5..7f49f013e9 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -462,6 +462,7 @@ Discourse::Application.routes.draw do get 'logs' => 'groups#histories' collection do + get 'custom/new' => 'groups#new', constraints: AdminConstraint.new get "search" => "groups#search" end diff --git a/spec/requests/groups_controller_spec.rb b/spec/requests/groups_controller_spec.rb index 5e47f4d95c..eefb9048e8 100644 --- a/spec/requests/groups_controller_spec.rb +++ b/spec/requests/groups_controller_spec.rb @@ -984,4 +984,34 @@ describe GroupsController do end end end + + describe '#new' do + describe 'for an anon user' do + it 'should return 404' do + get '/groups/custom/new' + + expect(response.status).to eq(404) + end + end + + describe 'for a normal user' do + before { sign_in(user) } + + it 'should return 404' do + get '/groups/custom/new' + + expect(response.status).to eq(404) + end + end + + describe 'for an admin user' do + before { sign_in(Fabricate(:admin)) } + + it 'should return 404' do + get '/groups/custom/new' + + expect(response.status).to eq(200) + end + end + end end diff --git a/test/javascripts/acceptance/groups-test.js.es6 b/test/javascripts/acceptance/group-test.js.es6 similarity index 65% rename from test/javascripts/acceptance/groups-test.js.es6 rename to test/javascripts/acceptance/group-test.js.es6 index 78f5526841..0e0181085c 100644 --- a/test/javascripts/acceptance/groups-test.js.es6 +++ b/test/javascripts/acceptance/group-test.js.es6 @@ -1,5 +1,7 @@ import { acceptance, logIn } from "helpers/qunit-helpers"; +acceptance("Group"); + const response = object => { return [ 200, @@ -8,63 +10,6 @@ const response = object => { ]; }; -acceptance("Groups", { - beforeEach() { - server.get('/groups/snorlax.json', () => { // eslint-disable-line no-undef - return response({"basic_group":{"id":41,"automatic":false,"name":"snorlax","user_count":1,"alias_level":0,"visible":true,"automatic_membership_email_domains":"","automatic_membership_retroactive":false,"primary_group":true,"title":"Team Snorlax","grant_trust_level":null,"incoming_email":null,"has_messages":false,"flair_url":"","flair_bg_color":"","flair_color":"","bio_raw":"","bio_cooked":null,"public":true,"is_group_user":true,"is_group_owner":true}}); - }); - - // Workaround while awaiting https://github.com/tildeio/route-recognizer/issues/53 - server.get('/groups/snorlax/logs.json', request => { // eslint-disable-line no-undef - if (request.queryParams["filters[action]"]) { - return response({"logs":[{"action":"change_group_setting","subject":"title","prev_value":null,"new_value":"Team Snorlax","created_at":"2016-12-12T08:27:46.408Z","acting_user":{"id":1,"username":"tgx","avatar_template":"/images/avatar.png"},"target_user":null}],"all_loaded":true}); - } else { - return response({"logs":[{"action":"change_group_setting","subject":"title","prev_value":null,"new_value":"Team Snorlax","created_at":"2016-12-12T08:27:46.408Z","acting_user":{"id":1,"username":"tgx","avatar_template":"/images/avatar.png"},"target_user":null},{"action":"add_user_to_group","subject":null,"prev_value":null,"new_value":null,"created_at":"2016-12-12T08:27:27.725Z","acting_user":{"id":1,"username":"tgx","avatar_template":"/images/avatar.png"},"target_user":{"id":1,"username":"tgx","avatar_template":"/images/avatar.png"}}],"all_loaded":true}); - } - }); - } -}); - -QUnit.test("Browsing Groups", assert => { - visit("/groups"); - - andThen(() => { - assert.equal(count('.groups-table-row'), 2, 'it displays visible groups'); - assert.equal(find('.group-index-join').length, 1, 'it shows button to join group'); - assert.equal(find('.group-index-request').length, 1, 'it shows button to request for group membership'); - }); - - click('.group-index-join'); - - andThen(() => { - assert.ok(exists('.modal.login-modal'), 'it shows the login modal'); - }); - - click('.login-modal .close'); - - andThen(() => { - assert.ok(invisible('.modal.login-modal'), 'it closes the login modal'); - }); - - click('.group-index-request'); - - andThen(() => { - assert.ok(exists('.modal.login-modal'), 'it shows the login modal'); - }); - - click("a[href='/groups/discourse/members']"); - - andThen(() => { - assert.equal(find('.group-info-name').text().trim(), 'Awesome Team', "it displays the group page"); - }); - - click('.group-index-join'); - - andThen(() => { - assert.ok(exists('.modal.login-modal'), 'it shows the login modal'); - }); -}); - QUnit.test("Anonymous Viewing Group", assert => { visit("/groups/discourse"); diff --git a/test/javascripts/acceptance/groups-index-test.js.es6 b/test/javascripts/acceptance/groups-index-test.js.es6 new file mode 100644 index 0000000000..c6b1eaaef0 --- /dev/null +++ b/test/javascripts/acceptance/groups-index-test.js.es6 @@ -0,0 +1,43 @@ +import { acceptance, logIn } from "helpers/qunit-helpers"; + +acceptance("Groups"); + +QUnit.test("Browsing Groups", assert => { + visit("/groups"); + + andThen(() => { + assert.equal(count('.groups-table-row'), 2, 'it displays visible groups'); + assert.equal(find('.group-index-join').length, 1, 'it shows button to join group'); + assert.equal(find('.group-index-request').length, 1, 'it shows button to request for group membership'); + }); + + click('.group-index-join'); + + andThen(() => { + assert.ok(exists('.modal.login-modal'), 'it shows the login modal'); + }); + + click('.login-modal .close'); + + andThen(() => { + assert.ok(invisible('.modal.login-modal'), 'it closes the login modal'); + }); + + click('.group-index-request'); + + andThen(() => { + assert.ok(exists('.modal.login-modal'), 'it shows the login modal'); + }); + + click("a[href='/groups/discourse/members']"); + + andThen(() => { + assert.equal(find('.group-info-name').text().trim(), 'Awesome Team', "it displays the group page"); + }); + + click('.group-index-join'); + + andThen(() => { + assert.ok(exists('.modal.login-modal'), 'it shows the login modal'); + }); +}); diff --git a/test/javascripts/acceptance/groups-new-test.js.es6 b/test/javascripts/acceptance/groups-new-test.js.es6 new file mode 100644 index 0000000000..9a57899bb7 --- /dev/null +++ b/test/javascripts/acceptance/groups-new-test.js.es6 @@ -0,0 +1,72 @@ +import { acceptance, logIn } from "helpers/qunit-helpers"; + +acceptance("New Group"); + +QUnit.test("As an anon user", assert => { + visit("/groups"); + + andThen(() => { + assert.equal( + find('.groups-admin-dropdown').length, 0, + 'it should not display the admin dropdown' + ); + }); +}); + +QUnit.test("Creating a new group", assert => { + logIn(); + Discourse.reset(); + + visit("/groups"); + + selectKit('.groups-admin-dropdown').expand().selectRowByValue("new"); + fillIn("input[name='name']", '1'); + + andThen(() => { + assert.equal( + find('.tip.bad').text().trim(), I18n.t("groups.new.name.too_short"), + 'it should show the right validation tooltip' + ); + + assert.ok( + find("button[title='Create']:disabled").length === 1, + 'it should disable the save button' + ); + }); + + fillIn("input[name='name']", 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'); + + andThen(() => { + assert.equal( + find('.tip.bad').text().trim(), I18n.t("groups.new.name.too_long"), + 'it should show the right validation tooltip' + ); + }); + + fillIn("input[name='name']", ''); + + andThen(() => { + assert.equal( + find('.tip.bad').text().trim(), I18n.t("groups.new.name.blank"), + 'it should show the right validation tooltip' + ); + }); + + fillIn("input[name='name']", 'goodusername'); + + andThen(() => { + assert.equal( + find('.tip.good').text().trim(), I18n.t("groups.new.name.available"), + 'it should show the right validation tooltip' + ); + }); + + click(".groups-new-public-admission"); + + andThen(() => { + assert.equal( + find('groups-new-allow-membership-requests').length, 0, + 'it should disable the membership requests checkbox' + ); + }); +}); From bc4de7b5eca365e782bd2786a8e824d90b5b3998 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 27 Mar 2018 17:44:04 +0800 Subject: [PATCH 022/287] Make eslint happy. --- test/javascripts/acceptance/groups-index-test.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/javascripts/acceptance/groups-index-test.js.es6 b/test/javascripts/acceptance/groups-index-test.js.es6 index c6b1eaaef0..503372269f 100644 --- a/test/javascripts/acceptance/groups-index-test.js.es6 +++ b/test/javascripts/acceptance/groups-index-test.js.es6 @@ -1,4 +1,4 @@ -import { acceptance, logIn } from "helpers/qunit-helpers"; +import { acceptance } from "helpers/qunit-helpers"; acceptance("Groups"); From 37fa843efcf225c12607de444881c570145951e8 Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Tue, 27 Mar 2018 11:40:46 +0200 Subject: [PATCH 023/287] Allow pulling of Urdu translations from Transifex again The translations have been fixed. --- config/locales/client.ur.yml | 867 +++++- config/locales/server.ur.yml | 3347 ++++++++++++++++++++- plugins/poll/config/locales/server.ur.yml | 1 + script/pull_translations.rb | 3 - 4 files changed, 4080 insertions(+), 138 deletions(-) diff --git a/config/locales/client.ur.yml b/config/locales/client.ur.yml index 0e7c1899e3..6f089f082e 100644 --- a/config/locales/client.ur.yml +++ b/config/locales/client.ur.yml @@ -9,7 +9,7 @@ ur: js: number: format: - separator: "کوئی نہیں" + separator: "." delimiter: "،" human: storage_units: @@ -22,6 +22,9 @@ ur: kb: کے بی mb: ایم بی tb: ٹی بی + short: + thousands: "{{number}}k" + millions: "{{number}}M" dates: time: "h:mm a" timeline_date: "MMM YYYY" @@ -30,32 +33,47 @@ ur: full_no_year_no_time: "MMMM Do" long_with_year: "MMM D, YYYY h:mm a" long_with_year_no_time: "MMM D, YYYY" - wrap_ago: "٪{date} پہلے" + full_with_year_no_time: "MMMM Do, YYYY" + long_date_with_year: "MMM D, 'YY LT" + long_date_without_year: "MMM D, LT" + long_date_with_year_without_time: "MMM D, 'YY" + long_date_without_year_with_linebreak: "MMM D
    LT" + long_date_with_year_with_linebreak: "MMM D, 'YY
    LT" + wrap_ago: "%{date} قبل" tiny: + half_a_minute: "< 1منٹ" less_than_x_seconds: one: "< 1 سیکنڈ" - other: "< ٪{count} سیکنڈ" + other: "< %{count} سیکنڈ" x_seconds: one: "1 سیکنڈ" - other: "٪{count} سیکنڈ" + other: "%{count} سیکنڈ" + less_than_x_minutes: + one: "< 1منٹ" + other: "< %{count}منٹ " x_minutes: one: "1 منٹ" - other: "٪{count} منٹس" + other: "%{count} منٹس" about_x_hours: one: "1 گھنٹا" - other: "٪{count} گھنٹے" + other: "%{count} گھنٹے" x_days: one: "1 دن" - other: "٪{count} دن" + other: "%{count} دن" + x_months: + one: "1ماہ" + other: "%{count}مہینے" about_x_years: one: "1 سال" - other: "٪{count} سال" + other: "%{count} سال" over_x_years: one: "> 1 سال" - other: "> ٪{count} سال" + other: "> %{count} سال" almost_x_years: one: "1 سال" - other: "٪{count} سال" + other: "%{count} سال" + date_month: "MMM D" + date_year: "MMM 'YY" medium: x_minutes: one: "1 منٹ" @@ -66,6 +84,7 @@ ur: x_days: one: "1 دن" other: "%{count} دن" + date_year: "MMM D, 'YY" medium_with_ago: x_minutes: one: "1 منٹ قبل" @@ -88,6 +107,7 @@ ur: other: "%{count} سال بعد" previous_month: 'پچھلے ماہ' next_month: 'اگلے ماہ' + placeholder: تاریخ share: topic: 'اس ٹاپک کا لنک شیئرکریں' post: 'پوسٹ #%{postNumber}' @@ -95,41 +115,44 @@ ur: twitter: 'ٹوئٹر پر اس لنک کو شیئرکریں' facebook: 'فیس بک پر اس لنک کو شیئرکریں' google+: 'گُوگَل+ پر اس لنک کو شیئرکریں' - email: 'اس لنک کو ایک ای میل میں بھیجیں' + email: 'اس لنک کو ای میل میں بھیجیں' action_codes: - public_topic: "اس ٹاپک کو پبلک بنایا گیا ٪{کب}" - private_topic: "اس ٹاپک کو پرائیویٹ بنایا گیا ٪{کب}" - split_topic: "اس ٹاپک کو تقسیم کیا گیا ٪{کب}" - invited_user: "دعوت دی ٪{کسے} ٪{کب}" - invited_group: "دعوت دی ٪{کسے} ٪{کب}" - removed_user: "ہٹایا ٪{کسے} ٪{کب}" - removed_group: "ہٹایا ٪{کسے} ٪{کب}" + public_topic: "اس ٹاپک کو پبلک بنایا گیا %{when}" + private_topic: "اس ٹاپک کو ذاتی پیغام بنایا گیا %{when}" + split_topic: "اس ٹاپک کو تقسیم کیا گیا %{when}" + invited_user: "دعوت دی %{who} %{when}" + invited_group: "دعوت دی %{who} %{when}" + user_left: "%{who}نے خود کواِس پیغام سے ہٹا دیا %{when} " + removed_user: "ہٹایا %{who} %{when}" + removed_group: "ہٹایا %{who} %{when}" autoclosed: - enabled: 'بند کیا ٪{کب}' - disabled: 'کھولا ٪{کب}' + enabled: 'بند کیا %{when}' + disabled: 'کھولا %{when}' closed: - enabled: 'بند کیا ٪{کب}' - disabled: 'کھولا ٪{کب}' + enabled: 'بند کیا %{when}' + disabled: 'کھولا %{when}' archived: - enabled: 'آرکائیو کیا ٪{کب}' - disabled: 'آرکائیو سے نکال دیا ٪{کب}' + enabled: 'آرکائیو کیا %{when}' + disabled: 'آرکائیو سے ہٹا دیا %{when}' pinned: - enabled: 'پن کر دیا ٪{کب}' - disabled: 'پن ہٹا دیا ٪{کب}' + enabled: 'پن کر دیا %{when}' + disabled: 'پن ہٹا دیا %{when}' pinned_globally: - enabled: 'عالمی سطح پر پن کر دیا ٪{کب}' - disabled: 'پن ہٹا دیا ٪{کب}' + enabled: 'عالمی سطح پر پن کر دیا %{when}' + disabled: 'پن ہٹا دیا %{when}' visible: - enabled: 'مندرج ٪{کب}' - disabled: 'غیر مندرج ٪{کب}' + enabled: 'مندرج %{when}' + disabled: 'غیر مندرج %{when}' banner: - enabled: 'اِس کو بینر بنا لیا ٪{when}۔ یہ ہر صفحے کے سب سے اوپر دکھایا جائے گا جب تک صارف اِسے برخاست نہیں کر دیتا۔' - disabled: 'اِس بینر کو ہٹا دیا ٪{when}۔ یہ اب ہر صفحے کے سب سے اوپر دکھایا نہیں جائے گا۔' + enabled: 'اِس کو بینر بنا لیا %{when}۔ یہ ہر صفحے کے سب سے اوپر دکھایا جائے گا جب تک صارف اِسے برخاست نہیں کر دیتا۔' + disabled: 'اِس بینر کو ہٹا دیا %{when}۔ یہ اب ہر صفحے کے سب سے اوپر دکھایا نہیں جائے گا۔' topic_admin_menu: "ٹاپک کے ایڈمن کی کارروائیاں" wizard_required: "اپنے نئے ڈِسکورس پر خوش آمدید! سیٹ اَپ وزرڈ سے آغاز کرتے ہیں۔✨" - emails_are_disabled: "تمام سبکدوش ہونے والے ای میل کو عالمی سطح پر ایک منتظم کی طرف سے غیر فعال کر دیا گیا ہے. کسی بھی قسم کی ای میل اطلاعات نہیں بھیجی جائیں گی۔" - bootstrap_mode_enabled: "آپ کی نئی ویب سائٹ شروع کرنے کو آسان بنانے کے لئے، آپ بوٹسٹریپ کے موڈ میں ہیں۔ تمام نئے صارفین کے ٹرسٹ لَیول 1 دیا گیا ہے اور روزانہ کی ای میل اپ ڈیٹس فعال کیے جا چکے ہیں۔ یہ خود کار طریقے سے غیر فعال کر دیا جائے گا جب کل صارف شمار %{min_users} سے تجاوز کر جائے گا۔" - bootstrap_mode_disabled: "بوٹسٹریپ کا موڈ اگلے 24 گھنٹوں میں غیر فعال کر دیا جائے گا۔" + emails_are_disabled: " ای میل کو منتظم کی طرف سے غیر فعال کر دیا گیا ہے. کسی بھی قسم کی ای میل نہیں بھیجی جائیں گی۔" + bootstrap_mode_enabled: "آپ کی نئی ویب سائٹ شروع کرنے کو آسان بنانے کے لئے، آپ بُوٹسٹرَیپ مَوڈ میں ہیں۔ تمام نئے صارفین کے ٹرسٹ لَیول 1 دیا گیا ہے اور روزانہ کی ای میل اپ ڈیٹس فعال کیے جا چکے ہیں۔ یہ خود کار طریقے سے غیر فعال کر دیا جائے گا جب %{min_users} صارفین شمولیت اختیار کر لیں گے۔" + bootstrap_mode_disabled: "بُوٹسٹرَیپ مَوڈ 24 گھنٹوں کے اندر غیر فعال کر دیا جائے گا۔" + themes: + default_description: "ڈِیفالٹ" s3: regions: ap_northeast_1: "ایشیا پیسفک (ٹوکیو)" @@ -140,8 +163,10 @@ ur: cn_north_1: "چین (بیجنگ)" eu_central_1: "یورپی یونین (فرینکفرٹ)" eu_west_1: "یورپی یونین (آئر لینڈ)" + eu_west_2: "یورپی یونین (لندن)" sa_east_1: "ایشیا پیسفک (ساؤ پالو)" us_east_1: "امریکی مشرق (شمالی ورجینیا)" + us_east_2: "امریکی مشرق (اَوہائیو)" us_gov_west_1: "اے ڈبلیو ایس گَو کلاوڈ (امریکہ)" us_west_1: "امریکی مغرب (شمالی کیلی فورنیا)" us_west_2: "امریکی مغرب (اوریگن)" @@ -149,6 +174,7 @@ ur: not_implemented: "یہ خصوصیت ابھی تک لاگو نہیں کی گئی، معضرت!" no_value: "نہیں " yes_value: "ہاں " + submit: "شائع" generic_error: "معذرت، ایک تکنیکی خرابی کا سامنا کرنا پڑا ہے۔" generic_error_with_reason: "ایک تکنیکی خرابی پیش آئی: %{error}" sign_up: "سائن اپ" @@ -193,7 +219,7 @@ ur: pm_title: "تجویز کیے گئے پیغامات" about: simple_title: "بارے میں" - title: "٪{title} کے بارے میں" + title: "%{title} کے بارے میں" stats: "سائٹ کے اعدادوشمار" our_admins: "ہمارے ایڈمن" our_moderators: "ہمارے ماڈریٹرز" @@ -207,7 +233,7 @@ ur: user_count: "صارفین" active_user_count: "متحرک صارفین" contact: "ہم سے رابطہ کریں" - contact_info: "اس سائٹ کے بارے میں کسی اہم مسئلہ یا فوری معاملہ کی صورت میں، براہ مہربانی ٪{contact_info} پر رابطہ کریں۔" + contact_info: "اس سائٹ کے بارے میں کسی اہم مسئلہ یا فوری معاملہ کی صورت میں، براہ مہربانی %{contact_info} پر رابطہ کریں۔" bookmarked: title: "بُک مارک" clear_bookmarks: "بک مارکس ختم کریں" @@ -222,15 +248,14 @@ ur: remove: "بُک مارک ہٹائیں" confirm_clear: "کیا آپ کو یقین ہے کہ آپ اِس ٹاپک سے تمام بُک مارکس ہٹانا چاہتے ہیں؟" topic_count_latest: - one: "{{count}} نئے اور اپ ڈیٹ کیے گئے ٹاپک۔" - other: "{{count}} نئے اور اپ ڈیٹ کیے گئے ٹاپک۔" + one: "{{count}} نیا یا اَپ ڈیٹ کردہ ٹاپک دیکھیں۔" + other: "{{count}} نئے یا اَپ ڈیٹ کردہ ٹاپک دیکھیں۔" topic_count_unread: - one: "{{count}} نہ پڑھے گئے ٹاپک۔" - other: "{{count}} نہ پڑھے گئے ٹاپک۔" + one: "{{count}} غیر پڑھا ٹاپک دیکھیں۔" + other: "{{count}} غیر پڑھے ٹاپک دیکھیں۔" topic_count_new: - one: "{{count}} نئے ٹاپک۔" - other: "{{count}} نئے ٹاپک۔" - click_to_show: "ظاہر کرنے کے لئے کلک کریں۔" + one: "{{count}} نیا ٹاپک دیکھیں۔" + other: "{{count}} نئے ٹاپک دیکھیں۔" preview: "پیشگی دیکھنا" cancel: "منسوخ" save: "تبدیلیاں محفوظ کریں" @@ -238,10 +263,12 @@ ur: saved: "محفوظ کر لیا گیا ہے!" upload: "اَپ لوڈ" uploading: "اَپ لوڈ کیا جا رہا ہے..." - uploading_filename: "{{فائل کا نام}} اَپ لوڈ کیا جا رہا ہے..." + uploading_filename: "{{filename}} اَپ لوڈ کیا جا رہا ہے..." uploaded: "اَپ لوڈ کیا جا چکا ہے!" + pasting: "پیسٹ کیا جا رہا ہے..." enable: "فعال کریں" disable: "غیر فعال کریں" + continue: "جاری رکھی" undo: "کالعدم کریں" revert: "رِیوَرٹ" failed: "عمل ناکام رہا" @@ -333,8 +360,10 @@ ur: title: 'گروپ میں ترمیم کریں' full_name: 'پورا نام' add_members: "اراکین شامل کریں" - delete_member_confirm: "'٪ {username}' گروپ سے '٪ {group}' ہٹائیں؟" + delete_member_confirm: "'%{username}' کو گروپ سے '%{group}' ہٹائیں؟" name_placeholder: "گروپ کا نام، کوئی خالی جگہ نہیں، نامِ صارفین کے اصولوں کے مطابق" + public_admission: "صارفین کو آزادی سے گروپ میں شمولیت اختیار کرنے کی اجازت دیں (گروپ کا عوامی طور پر ظاہر ہونا لاذمی ہو)" + public_exit: "صارفین کو آزادانہ طور پر گروپ چھوڑنے کی اجازت دیں" empty: posts: "اس گروپ کے ارکان کی طرف سے کوئی پوسٹس نہیں ہیں۔" members: "اس گروپ میں کوئی ارکان ہیں۔" @@ -346,25 +375,66 @@ ur: join: "گروپ میں شامل ہوں" leave: "گروپ چھوڑ دیں" request: "گروپ میں شمولیت کیلئے درخواست کریں" + filter_name: "گروپ نام کے حساب سے فِلٹر کریں" + message: "پیغام" automatic_group: خودکار گروپ - closed_group: بند گروپ + close_group: گروپ بند کریں is_group_user: "آپ اس گروپ کے رکن ہیں" + is_group_owner: "آپ اِس گروپ کے ایک مالک ہیں" + allow_membership_requests: "صارفین کو گروپ کے مالکان کو رکنیت کی درخواستیں بھیجنے کی اجازت دیں" + membership_request_template: "رکنیت کی درخواست بھیجنے کے دوران صارفین کیلئے ظاہر کیے جانے والی پہلے سے تیار کردہ مثال" + membership_request: + submit: "درخواست بھیجیں" + title: "@%{group_name} میں شمولیت کی درخواست" + reason: "گروپ مالکان کو بتائیے کہ آپ اس گروپ میں شامل ہونے کے کیوں مستحق ہیں" membership: "ممبرشپ " name: "نام" user_count: "ممبران کی تعداد" bio: "گروپ کے بارے میں" - selector_placeholder: "ممبران شامل کریں" + selector_placeholder: "صارف نام درج کریں" owner: "مالِک" index: title: "گروپس" empty: "کوئی نظر آنے والے گروپ موجود نہیں ہیں۔" + all_groups: "تمام گروپس" + owner_groups: "گروپس جن کا میں مالک ہوں" + close_groups: "گروپس بند کریں" + automatic_groups: "خودکار گروپس" + public_groups: "عوامی گروپس" + my_groups: "میرے گروپس" + title: + one: "گروپ" + other: "گروپس" activity: "سرگرمی" - members: "ممبران" + add_members: + title: "ممبران شامل کریں" + description: "اِس گروپ کی رکنیت کو مینیج کریں" + usernames: "صارف نام" + as_owner: "صارف(ین) کو اِس گروپ کے مالک(ین) کے طور پر مقرر کریں" + 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} کو مالک کے طور پر ہٹائیں" topics: "ٹاپک" posts: "پوسٹ" mentions: "ذکر" messages: "پیغامات" + notification_level: "گروپ پیغامات کیلئے اطلاعات کا پہلے سے طے شدہ لَیوَل" + visibility_levels: + title: "کون اِس گروپ کو دیکھ سکتا ہے؟" + public: "ہر کوئی" + members: "گروپ کے مالکان، اراکین اور منتظمین" + staff: "گروپ کے مالکان اور اسٹاف" + owners: "گروپ کے مالکان اور منتظمین" alias_levels: + mentionable: "کون اِس گروپ کو @زکر کرسکتا ہے؟" + messageable: "کون اِس گروپ کو پیغام بھیج سکتا ہے؟" nobody: "کوئی بھی نہیں" only_admins: "صرف منتظمین" mods_and_admins: "صرف ثالث اور منتظمین" @@ -413,7 +483,7 @@ ur: '14': "زیرِاِلتوَاء" categories: all: "تمام زُمرَہ جات" - all_subcategories: "تمام" + all_subcategories: "%{categoryName}میں تمام " no_subcategory: "کوئی نہیں" category: "زمرہ" category_list: "زمرہ جات کی فہرست ظاہر کریں" @@ -452,6 +522,7 @@ ur: topics_entered: " داخل ہوئے ٹاپک" post_count: "# پوسٹ" confirm_delete_other_accounts: "کیا آپ واقعی یہ اکاؤنٹس حذف کرنا چاہتے ہیں؟" + powered_by: "ipinfo.io کے مرہونِ مِنَّت" user_fields: none: "(ایک آپشن منتخب کریں)" user: @@ -460,6 +531,7 @@ ur: mute: "خاموش کردیں" edit: "ترجیحات میں ترمیم کردیں" download_archive: + button_text: "تمام ڈاؤن لوڈ کریں" confirm: "کیا آپ واقعی اپنی پوسٹس ڈاؤن لوڈ کرنا چاہتے ہیں؟" success: "ڈاؤن لوڈ کا آغاز کر دیا گیا ہے، عمل مکمل ہونے پر آپ کو پیغام کے ذریعے مطلع کر دیا جائے گا۔" rate_limit_error: "پوسٹس فی دن ایک بار ہی ڈاؤن لوڈ کی جا سکتی ہیں، براہ مہربانی کل دوبارہ کوشش کریں۔" @@ -484,11 +556,14 @@ ur: disable: "اطلاعات غیر فعال کریں" enable: "اطلاعات فعال کریں" each_browser_note: "نوٹ: آپ کو ہر براؤزر پر اس سیٹنگ کو تبدیل کرنا ہوگا." + dismiss: 'بر خاست کریں' dismiss_notifications: "سب بر خاست کریں" dismiss_notifications_tooltip: "تمام بغیر پڑھی اطلاعات کو پڑھی جا چکی اطلاعات کے طور پر مارک کریں" first_notification: "آپ کی پہلی نوٹیفکیشن! شروع کرنے کے لئے اسے منتخب کریں۔" disable_jump_reply: "میرے جواب دینے کے بعد میری پوسٹ پر نہ جائیں" dynamic_favicon: "براؤزر آئکن پر نئے / اَپ ڈیٹ ہوئے ٹاپکس کی گنتی دکھائیں" + theme_default_on_all_devices: "میری تمام ڈیوائسز پر اِس کو میری ڈیفالٹ تھیم بنائیں" + allow_private_messages: "دوسرے صارفین کو مجھے ذاتی پیغامات بھیجنے کی اجازت دیں" external_links_in_new_tab: "تمام بیرونی ویب سائٹ کے لنکس ایک نئے ٹیب میں کھولیں" enable_quoting: "روشنی ڈالے گئے ٹَیکسٹ کے لئے اقتباسی جواب فعال کریں" change: "بدلیں" @@ -496,7 +571,9 @@ ur: admin: "{{user}} ایک ایڈمِن ہے" moderator_tooltip: "یہ صارف ایک ماڈریٹر ہے" admin_tooltip: "یہ صارف ایک ایڈمِن ہے" - suspended_notice: "ہ صارف {{date}} تک معطل ہے۔" + silenced_tooltip: "یہ صارف خاموش کیا ہوا ہے" + suspended_notice: "یہ صارف {{date}} تک معطل ہے۔" + suspended_permanently: "یہ صارف معطل ہے۔" suspended_reason: "وجہ:" github_profile: "گِٹ حَب" email_activity_summary: "سرگرمی کا خلاصہ" @@ -508,7 +585,7 @@ ur: خاموش کردہ ٹاپک اور زمرہ جات اِن اِیمیلز میں شامل نہیں ہیں۔ individual: "ہر نئی پوسٹ پر ایک ای میل بھیجیں" individual_no_echo: "میری اپنی کے سوا، ہر نئی پوسٹ پر ایک ای میل بھیجیں" - many_per_day: "ہر نئی پوسٹ پر ایک ای میل بھیجیں (تقریبا {{یومیہ ای میل تخمینہ}} فی دن)" + many_per_day: "ہر نئی پوسٹ پر ایک ای میل بھیجیں (تقریباً {{dailyEmailEstimate}} فی دن)" few_per_day: "ہر نئی پوسٹ پر ایک ای میل بھیجیں (تقریبا 2 فی دن)" tag_settings: "ٹیگز" watched_tags: "دیکھا گیا" @@ -527,6 +604,7 @@ ur: watched_first_post_tags_instructions: "آپ کو اِن ٹیگ والے ہر نئے ٹاپک کی پہلی پوسٹ کے بارے میں مطلع کیا جائے گا۔" muted_categories: "خاموش کِیا ہوا" muted_categories_instructions: "آپ کو اِن زمرہ جات میں موجود نئے ٹاپکس کی کسی بھی چیز کے بارے میں مطلع نہیں کیا جائے گا، اور یہ تازہ ترین میں بھی نظر نہیں آئیں گے۔" + no_category_access: "ایک ماڈریٹر کے طور پر آپ کو زمرہ پر محدود رسائی حاصل ہے، محفوظ کرنا غیر فعال ہے۔" delete_account: "میرا اکاؤنٹ حذف کریں" delete_account_confirm: "کیا آپ واقعی مستقل طور پر اپنا اکاؤنٹ حذف کرنا چاہتے ہیں؟ اس عمل کو کالعدم نہیں کیا جا سکتا!" deleted_yourself: "آپ کے اکاؤنٹ کو کامیابی سے حزف کر دیا گیا ہے۔" @@ -538,11 +616,15 @@ ur: muted_users_instructions: "اِن صارفین کی طرف سے تمام اطلاعات کو روکیں۔" muted_topics_link: "خاموش کردیے گئے ٹاپکس دکھائیں" watched_topics_link: "دیکھے گئے ٹاپک ظاہر کریں" + tracked_topics_link: "ٹرَیک کیے گئے ٹاپک دکھائیں" automatically_unpin_topics: "جب میں سب سے نیچے تک پہنچوں تو خود کار طریقے سے ٹاپک سے پن ہٹایں۔" apps: "ایپس" revoke_access: "رسائی کالعدم کریں" undo_revoke_access: "رسائی کالعدم کو منسوخ کریں" api_approved: "منظورشدہ" + theme: "تھیم" + home: "ڈیفالٹ ہوم پیج" + staged: "سٹَیجڈ" staff_counters: flags_given: "مدد گار فلَیگ" flagged_posts: "فلَیگ کی گئی پوسٹس" @@ -560,6 +642,16 @@ ur: move_to_archive: "آر کائیو" failed_to_move: "منتخب شدہ پیغامات کو منتقل کرنے میں ناکامی (شاید آپ کے نیٹ ورک بند ہے)" select_all: "تمام منتخب کریں" + tags: "ٹیگز" + preferences_nav: + account: "اکاؤنٹ" + profile: "پروفائل" + emails: "اِی مَیل" + notifications: "اطلاعات" + categories: "زُمرَہ جات" + tags: "ٹیگز" + interface: "انٹرفیس" + apps: "اَیپس" change_password: success: "(اِی میل بھیج دی گئ)" in_progress: "(اِی میل بھیجی جا رہی ہے)" @@ -568,6 +660,19 @@ ur: set_password: "پاس ورڈ رکھیں" choose_new: "نیا پاس ورڈ منتخب کریں" choose: "پاس ورڈ منتخب کریں" + second_factor: + title: "دو فیکٹر توثیق" + disable: "دو فیکٹر توثیق غیر فعال کریں" + enable: "بہتر اکاؤنٹ سیکورٹی کیلئے" + confirm_password_description: "جاری رکھنے کیلئے براہ کرم اپنے پاسوَرڈ کی تصدیق کریں" + label: "کَوڈ" + enable_description: | + اِس QR کَوڈ کو کسی قابل اَیپ (اینڈرائڈiOSوِنڈَوز فون) میں اسکَین کریں اور اپنا توثیقی کَوڈ جمع کریں۔ + disable_description: "براہ مہربانی اپنی اَیپ میں سے توثیقی کَوڈ درج کریں" + show_key_description: "دستی طور پر درج کریں" + extended_description: | + دو فیکٹر توثیق پاسوَرڈ کے ساتھ ساتھ ایک دفعہ کے ٹَوکن کو مانگ کر آپ کے اکاؤنٹ کیلئے اضافی سیکورٹی شامل کرتا ہے۔ ٹَوکن اینڈرائڈ، iOS، اور وِنڈَوز فون ڈیوائسوں پر تخلیق کیے جا سکتے ہیں۔ + oauth_enabled_warning: "براہ مہربانی نوٹ کریں کہ ایک بار جب آپ کے اکاؤنٹ پر دو فیکٹر توثیق فعال ہو جائے تو سَوشَل لاگ اِن غیر فعال ہوجائیں گے۔" change_about: title: "\"میرے بارے میں\" تبدیل کریں" error: "اِس چیز کو تبدیل کرنے میں ایک خرابی کا سامنا کرنا پڑا۔" @@ -581,10 +686,12 @@ ur: taken: "معذرت، یہ اِی میل دستیاب نہیں ہے۔" error: "آپ کا اِی میل تبدیل کرنے میں ایک خرابی کا سامنا کرنا پڑا۔ شاید یہ ایڈریس پہلے سے استعمال میں ہے؟" success: "ہم نے اِس ایڈریس پر ایک اِی میل بھیج دی ہے۔ براہ کرم، تصدیق کی ہدایات پر عمل کریں۔" + success_staff: "ہم نے آپ کے موجودہ ایڈریس پر ایک اِی میل بھیج دی ہے۔ براہ کرم، تصدیق کی ہدایات پر عمل کریں۔" change_avatar: title: "اپنے پروفائل کی تصویر تبدیل کریں" gravatar: "گَرِیوَّٹار، کی بنیاد پر" gravatar_title: "گَرِیوَّٹار کی ویب سائٹ پر اپنے اوتار کو تبدیل کریں" + gravatar_failed: "گَرَیوَٹار درآمد نہیں کرایا جاسکا۔ کیا اس ای میل ایڈریس کے ساتھ ایک منسلک ہے؟ " refresh_gravatar_title: "اپنے گَرِیوَّٹار کو رِیفریش کریں" letter_based: "سسٹم تفویض کردہ پروفائل تصویر" uploaded_avatar: "اپنی مرضی کی تصویر" @@ -604,7 +711,7 @@ ur: instructions: "کبھی بھی عوام کو نہیں دکھایا گیا" ok: "ہم تصدیق کے لئے آپ کو اِی میل کریں گے" invalid: "براہ کرم، ایک قابلِ قبول ایِ میل ایڈریس درج کریں" - authenticated: "آپ کے اِی میل کی توثیق کر دی گئی ہے {{فراہم کنندہ}}" + authenticated: "آپ کے اِی میل کی توثیق کر دی گئی ہے {{provider}}" frequency_immediately: "جس بارے میں ہم آپ کو اِی میل کر رہے ہیں، اگر آپ نے نہیں پڑھی ہوئی تو ہم فوری طور پر آپ کو اِی میل کریں گے۔" frequency: one: "ہم آپ کو اِی میل کریں گے صرف اُس صور میں کہ ہم نے آپ کو گزشتہ ایک منٹ میں نہ دیکھا ہو۔" @@ -694,7 +801,7 @@ ur: title: "دعوتیں" user: "مدعو کیا گیا صارف" sent: "بھیج دیا گیا" - none: "ظاہر کرنے کے لئے کوئی زیرِاِلتوَاء دعوتیں نہیں ہیں۔" + none: "ظاہر کرنے کے لئے کوئی دعوتیں نہیں ہیں۔" truncated: one: "پہلی دعوت دکھائی جا رہی ہیں۔" other: "پہلی {{count}} دعوتیں دکھائی جا رہی ہیں۔" @@ -710,8 +817,12 @@ ur: expired: "اِس دعوت کی میعاد ختم ہو چکی ہے۔" rescind: "خارج کریں" rescinded: "دعوت خارج کر دی گئی" + rescind_all: "تمام دعوتیں منسوخ کر دیں" + rescinded_all: "تمام دعوتیں منسوخ کر دی گئیں!" + rescind_all_confirm: "کیا آپ واقعی تمام دعوتیں منسوخ کر دینا چاہتے ہیں؟" reinvite: "دعوت دوبارہ بھیجیں" reinvite_all: "تمام دعوتیں دوبارہ بھیجیں" + reinvite_all_confirm: "کیا آپ واقعی تمام دعوتیں دوبارہ بھیجنا چاہتے ہیں؟" reinvited: "دعوت دوبارہ بھیج دی گئی" reinvited_all: "تمام دعوتیں دوبارہ بھیج دی گئیں!" time_read: "پڑھنے کیلئے اِستعمال ہونے والا وقت" @@ -720,7 +831,7 @@ ur: create: "ایک دعوت بھیجیں" generate_link: "دعوت لنک کاپی کریں" link_generated: "دعوت لنک کامیابی سے بن گیا!" - valid_for: "دعوت لنک صرف اِس اِی میل اِیڈریس کے لئے درست ہے:٪{اِیمیل}" + valid_for: "دعوت لنک صرف اِس اِی میل اِیڈریس کے لئے درست ہے:%{email}" bulk_invite: none: "آپ نے یہاں ابھی تک کسی کو بھی مدعو نہیں کیا۔ انفرادی طار پر دعوت نامے ارسال کریں، یا ایک CSV فائل اَپ لوڈ کرکے ایک وقت میں ہی بہت سے لوگوں کو دعوت دیں۔" text: "فائل کی مدد سے ایک وقت میں بہت سے لوگوں کو دعوت دیں" @@ -738,15 +849,25 @@ ur: title: "خلاصہ" stats: "اعدادوشمار" time_read: "پڑھنے کیلئے اِستعمال ہونے والا وقت" + recent_time_read: "پڑھنے کیلئے اِستعمال ہونے والا حالیہ وقت" topic_count: one: "بنایا گیا ٹاپک" other: "بنائے گئے ٹاپک" post_count: one: "بنائی گئی پوسٹ" other: "بنائی گئیں پوسٹ" + likes_given: + one: "دیا گیا" + other: "دیا گیا" + likes_received: + one: "موصول ہوا" + other: "موصول ہوا" days_visited: one: "دن جس میں دورہ کیا گیا" other: "دن جن میں دورہ کیا گیا" + topics_entered: + one: "دیکھ لیا گیا ٹاپک" + other: "دیکھ لیے گئے ٹاپک" posts_read: one: "پڑھی گئی پوسٹ" other: "پڑھی گئیں پوسٹس" @@ -813,15 +934,15 @@ ur: enabled: "یہ سائٹ صرف پڑھنے کے مَوڈ میں ہے۔ براہِ مہربانی براؤز کرتے رہئیے، لیکن جواب دینا، لائکس دینا، اور دیگر اعمال ابھی کے لئے غیر فعال ہیں۔" login_disabled: "جب تک سائٹ صرف پڑھنے کے مَوڈ میں ہے لاگ اِن غیر فعال رہے گا۔" logout_disabled: "جب تک سائٹ صرف پڑھنے کے مَوڈ میں ہے لاگ آؤٹ غیر فعال رہے گا۔" - too_few_topics_and_posts_notice: "چلیں اِس بحث کو شروع کریں! اِس وقت اَبھی ٪{موجودہ ٹاپک}/٪{درکار ٹاپک} ٹاپک اور ٪{موجودہ پوسٹ}/٪{درکار پوسٹ} پوسٹ ہیں۔ نئے زائرین کو پڑھنے اور اُن کا جواب دینے کیلئے کچھ مکالمات کی ضرورت ہے۔" - too_few_topics_notice: "چلیں اِس بحث کو شروع کریں! اِس وقت اَبھی ٪{موجودہ ٹاپک}/٪{درکار ٹاپک} ٹاپک ہیں۔ نئے زائرین کو پڑھنے اور اُن کا جواب دینے کیلئے کچھ مکالمات کی ضرورت ہے۔" - too_few_posts_notice: "چلیں اِس بحث کو شروع کریں! اِس وقت اَبھی ٪{موجودہ پوسٹ}/٪{درکار پوسٹ} پوسٹ ہیں۔ نئے زائرین کو پڑھنے اور اُن کا جواب دینے کیلئے کچھ مکالمات کی ضرورت ہے۔" + 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} کی سائٹ سیٹِنگ لِمٹ سے تجاوز کر گئی۔" + reached: "%{relativeAge}%{rate} سائٹ سیٹِنگ لِمٹ %{siteSettingRate} تک پہنچ گئی۔" + exceeded: "%{relativeAge}%{rate} سائٹ سیٹِنگ لِمٹ %{siteSettingRate} سے تجاوز کر گئی۔" rate: - one: "1 خرابی/٪{مدت}" - other: "٪{count} خرابیاں/٪{duration}" + one: "1 خرابی/%{duration}" + other: "%{count} خرابیاں/%{duration}" learn_more: "اورجانیے..." all_time: 'کُل' all_time_desc: 'کُل ٹاپک بنائے گئے' @@ -835,6 +956,11 @@ ur: first_post: پہلی پوسٹ mute: خاموش کریں unmute: آواز چالو کریں + last_post: پوسٹ کیا + time_read: پڑھا + time_read_recently: 'حال ہی میں %{time_read}' + time_read_tooltip: 'کُل وقت پڑھا %{time_read}' + time_read_recently_tooltip: 'کُل وقت پڑھنے میں لگا %{time_read}(%{recent_time_read} گزشتہ 60 دنوں میں) ' last_reply_lowercase: آخری جواب replies_lowercase: one: جواب @@ -860,6 +986,7 @@ ur: private_message_info: title: "پیغام" invite: "دوسروں کو مدعو کریں..." + leave_message: "کیا آپ واقعی یہ پیغام بھیجنا چاہتے ہیں؟" remove_allowed_user: "کیا آپ واقعی اِس پیغام سے {{name}} ہٹانا چاہتے ہیں؟" remove_allowed_group: "کیا آپ واقعی اِس پیغام سے {{name}} ہٹانا چاہتے ہیں؟" email: 'اِی میل' @@ -870,7 +997,7 @@ ur: trust_level: 'ٹرسٹ لَیول' search_hint: 'صارف نام، ای میل یا IP ایڈریس' create_account: - disclaimer: "رجسٹر کرنے پر آپ <{{a href='{{privacy link>زاتی معلومات کی حفاظتی پالیسی اور سروس کی شرائط۔" + disclaimer: "رجسٹر کرنے پر آپ پرائیوِیسی پالیسی اور سروس کی شرائطسے اتفاق کرتے ہیں۔" title: "نیا اکاؤنٹ بنائیں" failed: "کچھ غلط ہو گیا، شاید یہ ای میل پہلے ہی سے رجسٹرڈ ہے، پاسورڈ بھول جانے والا لنک اِستعمال کر کے دیکھیں" forgot_password: @@ -878,20 +1005,35 @@ ur: action: "میں اپنا پاسورڈ بھول گیا" invite: "اپنا صارف نام یا اِی میل ایڈریس درج کریں، اور ہم آپ کو ایک پاسورڈ رِی سَیٹ ایمیل بھیج دیں گے۔" reset: "پاسورڈ رِی سَیٹ کریں" - complete_username: "اگر کوئی اکاؤنٹ username ٪{username} سے ملتا ہو گا، تو آپ کو پاسورڈ رِی سَیٹ کرنے کے لئے ہدایات کی ایک اِی میل جلد ہی موصول ہو جائے گی۔" - complete_email: "اگر کوئی اکاؤنٹ ٪{اِیمیل} سے ملتا ہو گا، تو آپ کو پاسورڈ رِی سَیٹ کرنے کے لئے ہدایات کی ایک اِی میل جلد ہی موصول ہو جائے گی۔" - complete_username_found: "ہمیں ایک اکاؤنٹ ملا ہے جو username ٪{username} سے میچ کرتا ہے، آپ کو پاسورڈ رِی سَیٹ کرنے کے لئے ہدایات کی ایک اِی میل جلد ہی موصول ہو جائے گی۔" - complete_email_found: "ہمیں ایک اکاؤنٹ ملا ہے جو ٪{اِیمیل} سے میچ کرتا ہے، آپ کو پاسورڈ رِی سَیٹ کرنے کے لئے ہدایات کی ایک اِی میل جلد ہی موصول ہو جائے گی۔" - complete_username_not_found: "کوئی اکاؤنٹ username ٪{username} سے میچ نہیں کرتا" - complete_email_not_found: "کوئی اکاؤنٹ ٪{اِیمیل} سے میچ نہیں کرتا" + complete_username: "اگر کوئی اکاؤنٹ %{username} سے میچ کرتا ہو گا، تو آپ کو پاسورڈ رِی سَیٹ کرنے کے لئے ہدایات کی ایک اِی میل جلد ہی موصول ہو جائے گی۔" + complete_email: "اگر کوئی اکاؤنٹ %{email} سے ملتا ہو گا، تو آپ کو پاسورڈ رِی سَیٹ کرنے کے لئے ہدایات کی ایک اِی میل جلد ہی موصول ہو جائے گی۔" + complete_username_found: "ہمیں ایک اکاؤنٹ ملا ہے جو %{username} سے میچ کرتا ہے، آپ کو پاسورڈ رِی سَیٹ کرنے کے لئے ہدایات کی ایک اِی میل جلد ہی موصول ہو جائے گی۔" + complete_email_found: "ہمیں ایک اکاؤنٹ ملا ہے جو %{email} سے میچ کرتا ہے، آپ کو پاسورڈ رِی سَیٹ کرنے کے لئے ہدایات کی ایک اِی میل جلد ہی موصول ہو جائے گی۔" + complete_username_not_found: "کوئی اکاؤنٹ %{username} سے میچ نہیں کرتا" + complete_email_not_found: "کوئی اکاؤنٹ %{email} سے میچ نہیں کرتا" + help: "ای میل نہیں موصول ہر رہی؟ اپنے سپَیم فولڈر کو پہلے چیک کرلیجیے گا۔

    آپ کو شک ہے کہ کونیسا ای میل ایڈریس آپ نے استعمال کیا ہے؟ ایک ای میل ایڈریس درج کریں اور ہم آپ کو بتائیں گے اگر یہ ہمارے پاس موجود ہے۔

    اگر آپ کو اپنے اکاؤنٹ پر ای میل ایڈریس تک اب رسائی حاصل نہیں ہے، تو براہ کرم ہمارے مددگار اسٹاف سے رابطہ کریں۔

    " + button_ok: "ٹھیک ہے" + button_help: "مدد" + 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: "براہ مہربانی اپنی اَیپ میں سے توثیقی کَوڈ درج کریں:" email_placeholder: "اِی میل یا صارف نام" caps_lock_warning: "کیپس لاک آن ہے" error: "نامعلوم خرابی" rate_limit: "دوبارہ لاگ اِن کرنے کی کوشش کرنے سے پہلے براہ کرم تھوڑا انتظار کریں۔" + blank_username: "براہ مہربانی اپنا اِیمیل یا صارف نام درج کریں۔" blank_username_or_password: "براہ مہربانی اپنا اِیمیل یا صارف نام، اور پاسورڈ درج کریں۔" reset_password: 'پاسورڈ رِی سَیٹ کریں' logging_in: "لاگ اِن ہو رہا ہے..." @@ -904,6 +1046,11 @@ ur: not_allowed_from_ip_address: "آپ اِس آئی پی ایڈریس سے لاگ اِن نہیں ہو سکتے۔" admin_not_allowed_from_ip_address: "آپ ایڈمن کے طور پر اِس آئی پی ایڈریس سے لاگ اِن نہیں ہو سکتے۔" resend_activation_email: "دوبارہ ایکٹیویشن ای میل بھیجنے کے لئے یہاں کلک کریں۔" + omniauth_disallow_totp: "آپ کے اکاؤنٹ پر دو فیکٹر توثیق فعال ہے۔ براہ مہربانی اپنے پاسوَرڈ کے ساتھ لاگ اِن کریں۔" + resend_title: "ایکٹیویشن اِیمیل دوبارہ بھیجییں" + change_email: "اِیمیل ایڈریس تبدیل کریں" + provide_new_email: "ایک نیا ایڈریس فراہم کریں اور ہم آپ کی تصدیقی ای میل دوبارہ بھیجیں گے۔" + submit_new_email: "اِیمیل ایڈریس اَپ ڈَیٹ کریں" sent_activation_email_again: "ہم نے آپ کو {{currentEmail}} پر ایک اور ایکٹیویشن اِی میل بھیجی ہے۔ اسے پہنچنے میں چند منٹ لگ سکتے ہیں؛ اپنے سپیم فولڈر کو چیک کرنا نہ بھولیے گا۔" to_continue: "برائے مہربانی لاگ اِن کریں" preferences: "آپ کا اپنی صارف ترجیحات کو تبدیل کرنے کے لیے لاگ اِن ہونا ضروری ہے۔" @@ -932,34 +1079,71 @@ ur: message: "گِٹ ہَب کے زریعے تصدیق کی جا رہی ہے (یقینی بنائیں کہ پاپ اَپ بلاکرز فعال نہیں ہیں)" invites: accept_title: "دعوت نامہ" - welcome_to: "{SITE_NAME}٪ پر خوش آمدید!" + welcome_to: "%{site_name} پر خوش آمدید!" invited_by: "آپ کو جن کی طرف سے مدعو کیا گیا تھا:" social_login_available: "آپ اِس اِیمیل کا استعمال کرتے ہوئے کسی بھی سوشل لاگ اِن کے ساتھ سائن اِن بھی کرسکیں گے۔" - your_email: "آپ کے اکاؤنٹ کا اِیمیل ایڈریس ٪{اِیمیل} ہے۔" + your_email: "آپ کے اکاؤنٹ کا اِیمیل ایڈریس %{email} ہے۔" accept_invite: "دعوت قبول کریں" success: "آپ کا اکاؤنٹ بنا دیا گیا ہے اور اب آپ لاگ اِن کر سکتے ہیں۔" + name_label: "نام" + password_label: "پاس ورڈ رکھیں" + optional_description: "(اختیاری)" password_reset: - continue: "{SITE_NAME}٪ پر جاری رکھیں" + continue: "%{site_name} پر جاری رکھیں" emoji_set: apple_international: "Apple/International" google: "Google" twitter: "Twitter" emoji_one: "Emoji One" win10: "Win10" + google_classic: "گُوگَل کلاسِک" + facebook_messenger: "فَیس بُک مَیسنجر" category_page_style: categories_only: "صرف زمرہ جات" categories_with_featured_topics: "نمایاں ٹاپکس کے ساتھ زمرہ جات" - categories_and_latest_topics: "تازہ ترین ٹاپکس کے ساتھ زمرہ جات" + categories_and_latest_topics: "زُمرہ جات اور تازہ ترین ٹاپکس" + categories_and_top_topics: "زمرہ جات اور ٹاپ ٹاپکس" shortcut_modifier_key: shift: 'شفٹ' ctrl: 'Ctrl' alt: 'Alt' + select_kit: + default_header_text: منتخب کریں... + no_content: کوئی میل نہیں ملے + filter_placeholder: تلاش کریں... + create: "'{{content}}' بنائیں" + max_content_reached: "آپ صرف {{count}} اشیاء منتخب کرسکتے ہیں." + emoji_picker: + filter_placeholder: اِیمَوجی تلاش کریں + people: لوگ + nature: نَیچر + food: کھانا + activity: سرگرمی + travel: سفر + objects: اشیاء + celebration: جشن + custom: اپنی مرضی کے اِیمَوجی + recent: حال ہی میں استعمال کیے گئے + default_tone: کوئی جِلد رنگ نہیں + light_tone: ہلکا جِلد رنگ + medium_light_tone: درمیانا ہلکا جِلد رنگ + medium_tone: درمیانا جِلد رنگ + medium_dark_tone: درمیانا سیاہ جِلد رنگ + dark_tone: سیاہ جِلد رنگ + shared_drafts: + title: "مشترکہ ڈرافٹس" + notice: "یہ ٹاپک صرف اُن لوگوں کیلئے نظر آتا ہے جو {{category}} زُمرَہ کو دیکھ سکتے ہیں۔" + destination_category: "مطلوبہ زُمرَہ" + publish: "مشترکہ ڈرافٹس شائع کریں" + confirm_publish: "کیا آپ واقعی اِس ڈرافٹ کو شائع کرنا چاہتے ہیں؟" + publishing: "ٹاپک شائع کیا جا رہا ہے..." composer: emoji: "اِیمَوجی :)" more_emoji: "مزید..." options: "اختیارات" whisper: "سرگوشی" unlist: "غیر مندرج" + blockquote_text: "بلاک متن" add_warning: "یہ ایک آفیشل انتباہ ہے۔" toggle_whisper: "سرگوشی ٹَوگل کریں" toggle_unlisted: "غیر مندرج ٹَوگل کریں" @@ -969,12 +1153,14 @@ ur: saved_local_draft_tip: "مقامی طور پر محفوظ کر لیا گیا" similar_topics: "آپ کا ٹاپک ملتا ہے..." drafts_offline: "ڈرافٹس آف لائن" + group_mentioned_limit: "انتباہ! آپ نے {{group}} کا ذکر کیا ہے، تاہم اس گروپ کے ممبران کی تعداد، منتظم کی طرف سے مقرر کردہ {{max}} صارفین کے ذکر کی حد سے زیادہ ہے ۔ کسی کو بھی اطلاع نہیں دی جائے گی۔" group_mentioned: one: "{{group}} کا ذکر کر کہ، آپ 1 شخص کو مطلع کرنے لگے ہیں - کیا آپ واقعی یہ کرنا چاہتے ہیں؟" other: "{{group}} کا ذکر کر کہ، آپ {{count}} لوگوں کو مطلع کرنے لگے ہیں - کیا آپ واقعی یہ کرنا چاہتے ہیں؟" cannot_see_mention: category: "آپ نے {{username}} کا زکر کیا ہے لیکن چونکہ اُن کو اِس زمرے تک رسائی حاصل نہیں، اُن کو مطلع نہیں کیا جائے گا۔ آپ کو انہیں ایک ایسے گروپ میں شامل کرنے کی ضرورت ہے جسے اِس زمرے تک رسائی حاصل ہے۔" private: "آپ نے {{username}} کا زکر کیا ہے لیکن چونکہ وہ یہ ذاتی پیغام نہیں دیکھ سکتے اُن کو مطلع نہیں کیا جائے گا۔ آپ کو اُنہیں اِس ذاتی پیغام میں مدعو کرنے کی ضرورت ہے۔" + duplicate_link: "لگتا ہے کہ آپ کا {{domain}} لنک @{{username}} نے {{ago}} کو ٹاپک میں پہلے سے ہی ایک جواب میں پوسٹ کر دیا تھا - کیا آپ واقعی یہ دوبارہ پوسٹ کرنا چاہتے ہیں؟" error: title_missing: "عنوان درکار ہے" title_too_short: "عنوان میں کم از کم {{min}} حروف ہونا ضروری ہے" @@ -990,6 +1176,8 @@ ur: cancel: "منسوخ" create_topic: "ٹاپک بنائیں" create_pm: "پیغام" + create_whisper: "سرگوشی" + create_shared_draft: "مشترکہ ڈرافٹ بنائیں" title: "یا Ctrl+Enter دبائیں" users_placeholder: "ایک صارف شامل کریں" title_placeholder: "ایک مختصر جملہ میں بتائیے کہ یہ بحث کس چیز کے بارے میں ہے؟" @@ -997,7 +1185,9 @@ ur: edit_reason_placeholder: "آپ ترمیم کیوں کر رہے ہیں؟" show_edit_reason: "(ترمیم کی وجہ شامل کریں)" topic_featured_link_placeholder: "عنوان کے ساتھ دکھایا گیا لنک درج کریں۔" + remove_featured_link: "ٹاپک سے لنک ہٹا ئیں۔" reply_placeholder: "یہاں ٹائپ کریں۔ فارمیٹ کیلئے مارکڈائون، BBCode، یا HTML اِستعمال کریں۔ تصاویر ڈریگ یا پیسٹ کریں۔" + reply_placeholder_no_images: "یہاں ٹائپ کریں۔ فارمیٹ کیلئے مارکڈائون، BBCode، یا HTML اِستعمال کریں۔" view_new_post: "اپنے نئی پوسٹ دیکھئیے۔" saving: "محفوظ کیا جا رہا ہے" saved: "محفوظ کر لیا گیا!" @@ -1009,6 +1199,7 @@ ur: bold_label: "B" bold_text: "گہرا ٹَیکسٹ" italic_label: "I" + italic_title: "آئیٹیلک" italic_text: "زور دیا گیا ٹَیکسٹ" link_title: "ہائپرلِنک" link_description: "یہاں لِنک کی تفصیل درج کریں" @@ -1025,20 +1216,63 @@ ur: olist_title: "نمبروار فہرست" ulist_title: "بلٹ والی لسٹ" list_item: "فہرست آئٹم" + toggle_direction: "سمت ٹَوگل کریں" help: "مارکڈائون ترمیم میں مدد" + collapse: "کمپوزر پینل کو مینِمائز کریں" + abandon: "کمپوزر بند اور ڈرافٹ ختم کردیں" modal_ok: "ٹھیک" modal_cancel: "منسوخ" - cant_send_pm: "معذرت، آپ ٪{username} کو پیغام نہیں بھیج سکتے۔" + cant_send_pm: "معذرت، آپ %{username} کو پیغام نہیں بھیج سکتے۔" yourself_confirm: title: "کیا آپ وصول کنندگان کو شامل کرنا بھول گئے؟" body: "اِس وقت یہ پیغام صرف اپنے آپ کو بھیجا جا رہا ہے!" admin_options_title: "اس ٹاپک کیلئے عملے کی اختیاری سیٹِنگ" + composer_actions: + reply_to_post: + label: '%{postUsername} کی طرف سے پوسٹ %{postNumber} کا جواب دیں' + desc: ایک مخصوص پوسٹ پر جواب دیں + reply_as_new_topic: + label: مُنسلِک ٹاپک کے طور پر جواب دیں + desc: اِس ٹاپک سے منسلک ایک نیا ٹاپک بنائیں + reply_as_private_message: + label: نیا پیغام + desc: نیا ذاتی پیغام بنائیں + reply_to_topic: + label: ٹاپک پر جواب دیں + desc: ٹاپک کا جواب دیں، نہ کہ کسی مخصوص پوسٹ کا + toggle_whisper: + label: سرگوشی ٹَوگل کریں + desc: سرگوشیاں صرف سٹاف اراکین کو نظر آتی ہیں + create_topic: + label: "نیا ٹاپک" + shared_draft: + label: "مشترکہ ڈرافٹ" + desc: "ایک ٹاپک ڈرافٹ کریں جو صرف سٹاف کو نظر آسکے" notifications: + tooltip: + regular: + one: "1 اندیکھی اطلاع" + other: "{{count}} اندیکھی اطلاعات" + message: + one: "1 غیر پڑھا پیغام" + other: "{{count}} غیر پڑھے پیغامات" title: "@نام کے ذکر، آپ کی پوسٹ اور ٹاپک پر جوابات، پیغامات، وغیرہ کی اطلاعات" none: "اِس وقت ویب سائٹ اطلاعات لوڈ کرنے سے قاصر ہے۔" empty: "کوئی اطلاعات نہیں ملیں۔" more: "پرانی اطلاعات دیکھیے" total_flagged: "کُل فلَیگ کی گئی پوسٹس" + mentioned: "{{username}} {{description}}" + group_mentioned: "{{username}} {{description}}" + liked_many: + one: "{{username2}}، {{username}} اور 1 مزید {{description}}" + other: "{{username2}}، {{username}} اور {{count}} مزید {{description}}" + invitee_accepted: "{{username}}نے آپ کی دعوت قبول کرلی " + moved_post: "{{username}}نے {{description}} منتقل کر دیا" + granted_badge: "'{{description}}' حاصل کیا" + watching_first_post: "نیا ٹاپک {{description}}" + group_message_summary: + one: "آپ کے {{group_name}} اِن باکس میں {{count}} پیغام" + other: "آپ کے {{group_name}} اِن باکس میں {{count}} پیغامات" alt: mentioned: "کی طرف سے ذکر کیا گیا" quoted: "کی طرف سے اقتباس کیا گیا" @@ -1054,6 +1288,7 @@ ur: linked: "آپ کی پوسٹ سے لنک کیا" granted_badge: "بَیج عطا کیا" group_message_summary: "گروپ اِن باکس میں پیغامات" + topic_reminder: "ایک یاد دہانی" popup: mentioned: '{{username}} نے آپ کا تذکرہ "{{topic}}" میں کیا - {{site_title}}' group_mentioned: '{{username}} نے آپ کا تذکرہ "{{topic}}" میں کیا - {{site_title}}' @@ -1076,43 +1311,66 @@ ur: uploading: "اَپ لوڈ کیا جا رہا ہے" select_file: "فائل منتخب کریں" image_link: "لنک جس سے آپ کی تصویر منسلک ہو گی" + default_image_alt_text: تصویر search: sort_by: "ترتیب بہ" relevance: "مطابقت" latest_post: "تازہ ترین پوسٹ" + latest_topic: "تازہ ترین ٹاپک" most_viewed: "سب سے زیادہ دیکھا گیا" most_liked: "سب سے زیادہ لائک کیا گیا" select_all: "تمام منتخب کریں" clear_all: "تمام کو صاف کریں" too_short: "آپ کا سَرچ ٹَرم بہت مختصر ہے۔" + result_count: + one: "{{term}} کیلئیے 1 نتیجہ" + other: "{{term}} کیلئیے {{count}}{{plus}} نتائج" title: "ٹاپک، پوسٹس، صارفین، یا زمرہ جات کو سَرچ کریں" no_results: "کوئی نتائج نہیں پائے گئے۔" no_more_results: "کوئی اور نتائج نہیں پائے گئے۔" searching: "تلاش کیا جا رہا ہے ..." post_format: "{{username}} کی طرف سے #{{post_number}}" + results_page: "'{{term}}' کیلئے سرچ کے نتائج" + more_results: "مزید نتائج موجود ہیں۔ برائے مہربانی اپنے سرچ کے معیار کو محدود کریں۔" + cant_find: "آپ جو ڈھونڈ رہے تھے وہ نہیں مل سکا؟" + start_new_topic: "شاید ایک نیا ٹاپک شروع کریں؟" + or_search_google: "یا اس کے بجائے گُوگَل کے ساتھ تلاش کرنے کی کوشش کریں:" + search_google: "اس کے بجائے گُوگَل کے ساتھ تلاش کرنے کی کوشش کریں:" + search_google_button: "گُوگَل" + search_google_title: "اِس سائٹ میں تلاش کریں" context: user: "@{{username}} کے حساب سے پوسٹس تلاش کریں" - category: "#{{category}} category میں تلاش کریں" + category: "#{{category}} زُمرَہ میں تلاش کریں" topic: "اِس ٹاپک میں تلاش کریں" private_messages: "پیغامات میں تلاش کریں" advanced: title: اعلی درجے کی تلاشی posted_by: label: کی طرف سے پوسٹ کیا گیا + in_category: + label: زمرہ میں ڈالا گیا in_group: label: گروپ میں with_badge: label: بَیج کے ساتھ + with_tags: + label: ٹیگ ہواوا filters: + label: صرف وہ ٹاپک/پوسٹس دکھائیں جو... likes: جو میں نے لائیک کیے posted: جن میں میں نے پوسٹ کیا watching: جو میں دیکھ رہا ہوں tracking: جو میں ٹریک کر رہا ہوں + private: میرے پیغامات میں ہوں + bookmarks: میں نے بُک مارک کیے ہوے ہوں first: جو سب سے پہلی پوسٹ ہو pinned: جو پِن ہوا ہو unpinned: جو پِن نہ ہوا ہو + seen: جو میں نے پڑھ لیے ہوں unseen: جو میں نے نہ پڑھا ہو wiki: جو وِیکی ہو + images: تصاویر شامل کریں + all_tags: تمام درجِ بالا ٹیگز statuses: label: جہاں ٹاپک open: کھلے ہوں @@ -1138,6 +1396,7 @@ ur: select_all: "تمام منتخب کریں" clear_all: "تمام کو صاف کریں" unlist_topics: "ٹاپکس کو فہرست سے ہٹائیں" + relist_topics: "ٹاپک دوبارہ فہرست کریں" reset_read: "\"پڑھ لیا گیا\" کو رِی سَیٹ کریں" delete: "ٹاپکس حذف کریں" dismiss: "بر خاست کریں" @@ -1148,8 +1407,10 @@ ur: dismiss_new: "نیا برخاست کریں" toggle: "ٹاپکس کے بَلک انتخاب کو ٹَوگل کریں" actions: "بَلک عمل" + change_category: "زمرہ تبدیل کریں" close_topics: "ٹاپکس بند کریں" archive_topics: "ٹاپکس آر کائیو کریں" + notification_level: "اطلاعات" choose_new_category: "ٹاپکس کیلئے نئے زمرہ کا انتخاب کریں:" selected: one: "آپ نے 1 ٹاپک منتخب کیا ہے۔" @@ -1200,6 +1461,9 @@ ur: move_to_inbox: title: 'اِنباکس میں منتقل کریں' help: 'پیغام واپس اِنباکس میں منتقل کریں' + edit_message: + help: 'پیغام کی پہلی اشاعت میں ترمیم کریں' + title: 'پیغام میں ترمیم کریں' list: 'ٹاپک' new: 'نیا ٹاپک' unread: 'بغیر پڑھے' @@ -1244,13 +1508,62 @@ ur: jump_reply_up: اِس سے پرانے جواب پر جائیں jump_reply_down: اِس سے نئے جواب پر جائیں deleted: "ٹاپک حذف کردیا گیا ہے" + topic_status_update: + title: "ٹاپک ٹائمر" + save: "ٹائمر مقرر کریں" + num_of_hours: "گھنٹوں کی تعداد:" + remove: "ٹائمر ہٹائیں" + publish_to: "شائع کریں:" + when: "کب:" + public_timer_types: ٹاپک ٹائمر + private_timer_types: صارف کے ٹاپک ٹائمر + auto_update_input: + none: "ایک ٹائم فریم منتخب کریں" + later_today: "آج بعد میں" + tomorrow: "کَل" + later_this_week: "اِس ہفتے بعد میں" + this_weekend: "اِس ہفتےکےآخر میں" + next_week: "اگلے ہفتے" + two_weeks: "دو ہفتے" + next_month: "اگلے ماہ" + three_months: "تین مہینے" + six_months: "چھ مہینے" + one_year: "ایک سال" + forever: "ہمیشہ کیلئے" + pick_date_and_time: "تاریخ اور وقت منتخب کریں" + set_based_on_last_post: "آخری پوسٹ کی بنیاد پر بند کریں" + publish_to_category: + title: "اشاعت شیڈول کریں" + temp_open: + title: "عارضی طور پر کھولیں" + auto_reopen: + title: "خود کار طریقے سے ٹاپک کھولیں" + temp_close: + title: "عارضی طور پر بند کریں" + auto_close: + title: "خود کار طریقے سے ٹاپک بند کریں" + label: "خود کار طریقے سے ٹاپک بند کرنے کے گھنٹے:" + error: "براہ کرم، ایک قابلِ قبول قدر درج کریں۔" + based_on_last_post: "بند نہ کریں جب تک ٹاپک کی آخری پوسٹ کم از کم اتنی پرانی نہ ہو۔" + auto_delete: + title: "خود کار طریقے سے ٹاپک حذف کریں" + reminder: + title: "مجھے یاد دہانی کرائیں" + status_update_notice: + auto_open: "یہ ٹاپک خود کار طریقے سے کھول دیا جائے گا %{timeLeft}۔" + auto_close: "یہ ٹاپک خود کار طریقے سے بند کر دیا جائے گا %{timeLeft}۔" + auto_publish_to_category: "یہ ٹاپک #%{categoryName} میں شائع کر دیا جائے گا %{timeLeft}۔" + auto_close_based_on_last_post: "یہ ٹاپک آخری جواب کے %{duration} بعد بند ہو جائے گا۔" + auto_delete: "یہ ٹاپک خود کار طریقے سے حذف کر دیا جائے گا %{timeLeft}۔" + auto_reminder: "آپ کو اس ٹاپک کے بارے میں یاد دہانی کرائی جائے گا %{timeLeft}۔" auto_close_title: 'خود کار طریقے سے بند کر نے کی سیٹِنگ' auto_close_immediate: one: "ٹاپک کی آخری پوسٹ ابھی سے 1 گھنٹا پرانی ہے، اِسلیئے ٹاپک فوری طور پر بند کر دیا جائے گا۔" - other: "ٹاپک کی آخری پوسٹ ابھی سے ٪{count} گھنٹے پرانی ہے، اِسلیئے ٹاپک فوری طور پر بند کر دیا جائے گا۔" + other: "ٹاپک کی آخری پوسٹ ابھی سے %{count} گھنٹے پرانی ہے، اِسلیئے ٹاپک فوری طور پر بند کر دیا جائے گا۔" timeline: back: "واپس" back_description: "آپنی آخری بغیر پڑھی پوسٹ پر واپس جائیں" + replies_short: "%{current}/ %{total} " progress: title: ٹاپک پیش رَفت go_top: "سب سے اوپر" @@ -1258,9 +1571,9 @@ ur: go: "جائیں" jump_bottom: "آخری پوسٹ پر جائیں" jump_prompt: "پر جائیں..." - jump_prompt_of: "٪{count} پوسٹس کے" + jump_prompt_of: "%{count} پوسٹس کے" jump_prompt_long: "کس پوسٹ پر آپ جانا پسند کریں گے؟" - jump_bottom_with_number: "پوسٹ ٪{post_number} پر جائیں" + jump_bottom_with_number: "پوسٹ %{post_number} پر جائیں" total: کُل پوسٹس current: موجودہ پوسٹ notifications: @@ -1273,6 +1586,10 @@ ur: '3_2': 'آپ کو اطلاعات موصول ہوں گی کیونکہ آپ اِس ٹاپک کو دیکھ رہے ہیں۔' '3_1': 'آپ کو اطلاعات موصول ہوں گی کیونکہ آپ نے یہ ٹاپک بنایا ہے۔' '3': 'آپ کو اطلاعات موصول ہوں گی کیونکہ آپ اِس ٹاپک کو دیکھ رہے ہیں۔' + '2_8': 'آپ نئے جوابات کا شمار دیکھ سکیں گے کیونکہ آپ اِس زمرہ کو ٹرَیک کر رہے ہیں۔' + '2_4': 'آپ نئے جوابات کا شمار دیکھ سکیں گے کیونکہ آپ نے اِس ٹاپک پر ایک جواب پوسٹ کیا۔' + '2_2': 'آپ نئے جوابات کا شمار دیکھ سکیں گے کیونکہ آپ اِس ٹاپک کو ٹرَیک کر رہے ہیں۔' + '2': 'آپ نئے جوابات کا شمار دیکھ سکیں گے کیونکہ آپ نے یہ ٹاپک پڑھا ہے۔' '1_2': 'اگر کوئی آپ کا @نام زکر کرتا ہے یا کوئی جواب دیتا ہے تو آپ کو مطلع کر دیا جائے گا۔' '1': 'اگر کوئی آپ کا @نام زکر کرتا ہے یا کوئی جواب دیتا ہے تو آپ کو مطلع کر دیا جائے گا۔' '0_7': 'آپ اِس زمرہ میں تمام اطلاعات کو نظر انداز کر رہے ہیں۔' @@ -1308,6 +1625,7 @@ ur: open: "ٹاپک کھولیں" close: "ٹاپک بند کریں" multi_select: "پوسٹس منتخب کریں..." + timed_update: "ٹاپک ٹائمر مقرر کریں..." pin: "ٹاپک پِن کریں..." unpin: "ٹاپک سے پِن ہٹائیں..." unarchive: "ٹاپک آرکائیو سے ختم کریں" @@ -1392,6 +1710,7 @@ ur: success_email: "ہم نے {{emailOrUsername}} کو ایک دعوت نامہ اِیمیل کیا ہے۔ جب دعوت نامہ اِستعمال ہو گا تو ہم آپ کو مطلع کریں گے۔ اپنی دعوتوں کا ٹریک رکھنے کیلئے اپنے صارف صفحہ پر دعوت ناموں والی ٹیب چیک کریں۔" success_username: "ہم نے اِس ٹاپک میں حصہ لینے کے لئے اُس صارف کو مدعو کیا ہے۔" error: "معزرت، ہم اس شخص کو دعوت نہ دے سکے۔ شاید وہ پہلے ہی مدعو کیے جا چکے ہیں؟ (دعوتیں شرح محدود ہیں)" + success_existing_email: "آیک صارف {{emailOrUsername}} والے ای میل کے ساتھ پہلے ہی موجود ہے۔ ہم نے اِس ٹاپک میں حصہ لینے کے لئے اُس صارف کو مدعو کر دیا ہے۔" login_reply: 'جواب دینے کیلئے لاگ اِن کریں' filters: n_posts: @@ -1428,22 +1747,36 @@ ur: other: "براہ مہربانی، {{old_user}} کی {{count}} پوسٹس کے نئے مالک کو منتخب کریں۔" instructions_warn: "یاد رکھیں کہ اِس پوسٹ کے بارے میں کوئی بھی اطلاعات مؤثر بہ ماضی طور پر نئے صارف کو منتقل نہیں کی جائیں گی۔
    انتباہ: فی الحال، کوئی پوسٹ انحصار ڈیٹا نئے صارف کو منتقل نہیں کیا جاتا ہے۔ احتیاط سے استعمال کریں۔" change_timestamp: + title: "ٹائمسٹیمپ تبدیل کریں..." action: "ٹائمسٹیمپ تبدیل کریں" invalid_timestamp: "ٹائمسٹیمپ مستقبل میں نہیں ہو سکتا۔" error: "ٹاپک کا ٹائمسٹیمپ تبدیل کرنے میں ایک خرابی کا سامنا کرنا پڑا۔" instructions: "براہ مہربانی ٹاپک کیلیئے نیا ٹائمسٹیمپ منتخب کریں۔ وقت کا فرق برابر رکھنے کیلیے ٹاپک میں پوسٹس کو اَپ ڈیٹ کر دیا جائے گا۔" multi_select: select: 'منتخب' - selected: 'منتخب کیے گئے ({{count}})' + selected: 'منتخب کردہ ({{count}})' + select_post: + label: 'منتخب' + title: 'انتخاب کیے ہوے میں پوسٹ شامل کریں' + selected_post: + label: 'منتخب کیا ہوا' + title: 'منتخب کیے ہوے سے پوسٹ کو ہٹانے کیلئے کلک کریں' + select_replies: + label: 'منتخب +جوابات' + title: 'منتخب کیے ہوے میں پوسٹ اور اِس کے تمام جوابات شامل کریں' + select_below: + label: 'منتخب+ذیل میں' + title: 'منتخب کیے ہوے میں پوسٹ اور اِس کے بعد تمام شامل کریں' delete: منتخب کیے ہوں کو حذف کریں cancel: انتخاب کرنا منسوخ کریں select_all: تمام منتخب کریں deselect_all: تمام غیر منتخب کریں description: - one: آپ نے 1 پوسٹ منتخب کی ہے۔ - other: آپ نے {{count}} پوسٹس منتخب کی ہیں۔ + one: "آپ نے 1 پوسٹ منتخب کی ہے۔" + other: "آپ نے {{count}} پوسٹس منتخب کی ہیں۔" post: quote_reply: "اقتباس کریں" + edit: "{{link}} {{replyAvatar}} {{username}} " edit_reason: "وجہ:" post_number: "پوسٹ {{number}}" wiki_last_edited_on: "وِکی کو آخری بار ترمیم کیا گیا" @@ -1455,9 +1788,11 @@ ur: show_full: "مکمل پوسٹ دکھائیں" show_hidden: 'چھپایا ہوا متن دکھایں۔' deleted_by_author: - one: "(مصنف نے پوسٹ واپس لے لی، ٪{count} گھنٹے میں یہ خود کار طریقے سے حذف کر دی جائے گی الا یہ کہ اِسے فلیگ کیا گیا ہو)" - other: "(مصنف نے پوسٹ واپس لے لی، ٪{count} گھنٹوں میں یہ خود کار طریقے سے حذف کر دی جائے گی الا یہ کہ اِسے فلیگ کیا گیا ہو)" + one: "(مصنف نے پوسٹ واپس لے لی، %{count} گھنٹے میں یہ خود کار طریقے سے حذف کر دی جائے گی الا یہ کہ اِسے فلیگ کیا گیا ہو)" + other: "(مصنف نے پوسٹ واپس لے لی، %{count} گھنٹوں میں یہ خود کار طریقے سے حذف کر دی جائے گی الا یہ کہ اِسے فلیگ کیا گیا ہو)" + collapse: "بند کریں" expand_collapse: "کھولیں/بند کریں" + locked: "ایک اسٹاف کے رکن نے اِس پوسٹ کو ترمیم ہونے سے روک دیا ہے" gap: one: "1 چھپایا ہوا جواب دیکھیں" other: "{{count}} چھپائے ہوے جوابات دیکھیں" @@ -1504,12 +1839,22 @@ ur: has_liked: " آپ اِس پوسٹ کو لائیک کر چکے ہیں" undo_like: "لائیک کالعدم کریں" edit: " اِس پوسٹ کو ترمیم کریں" + edit_action: "ترمیم" edit_anonymous: "معذرت، اِس پوسٹ کو ترمیم کرنے کیلیے آپ کا لاگ اِن ہونا ضروری ہے۔" flag: "اس پوسٹ پر توجہ کے لئے اِسے نجی طور پر فلَیگ کریں یا اس کے بارے میں ایک نجی نوٹیفکیشن بھیجیں" delete: "اِس پوسٹ کو حذف کریں" undelete: " اِس پوسٹ کو واپس لائیں" share: "اس پوسٹ کا لنک شیئرکریں" more: "مزید " + delete_replies: + confirm: "کیا آپ اِس پوسٹ پر جوابات بھی حذف کرنا چاہتے ہیں؟" + direct_replies: + one: "جی ہاں، اور براہ راست 1 جواب" + other: "جی ہاں، اور براہ راست {{count}} جوابات" + all_replies: + one: "جی ہاں، اور 1 جواب" + other: "جی ہاں، اور تمام {{count}} جوابات" + just_the_post: "نہیں، صرف یہ پوسٹ" admin: "پوسٹ ایڈمن کے ایکشن" wiki: "وِیکی بنائیں" unwiki: "وِیکی ختم کریں" @@ -1518,8 +1863,16 @@ ur: rebake: "HTML دوبارہ بِلڈ کریں" unhide: "چھپانا ختم کریں" change_owner: "پوسٹس کے مالک کو تبدیل کریں" + grant_badge: "بَیج دیں" + lock_post: "پوسٹ لاک کریں" + lock_post_description: "شائع کرنے والے کو اِس پوسٹ میں ترمیم کرنے سے روک دیں" + unlock_post: "پوسٹ کھول دیں" + unlock_post_description: "شائع کرنے والے کو اِس پوسٹ میں ترمیم کرنے کی اجازت دیں" actions: flag: 'فلَیگ' + defer_flags: + one: "فلَیگ نظر انداز کریں" + other: "فلَیگز نظر انداز کریں" undo: off_topic: "فلَیگ کالعدم کریں" spam: "فلَیگ کالعدم کریں" @@ -1535,6 +1888,9 @@ ur: notify_user: "ایک پیغام بھیجا" bookmark: "اِس کو بُک مارک" like: "اِس کو لائیک کیا" + like_capped: + one: "اور {{count}} دوسرے شخص نے اِس کو لائیک کیا" + other: "اور {{count}} دوسرے لوگوں نے اِس کو لائیک کیا" vote: "اِس کے لیے ووٹ دیا" by_you: off_topic: "آپ نے اِس کو موضوع سے ہٹ کر ہونے کے طور پر فلَیگ کیا" @@ -1595,6 +1951,14 @@ ur: vote: one: "1 شخص نے اِس پوسٹ کے لیے ووٹ کیا" other: "{{count}} لوگوں نے اِس پوسٹ کے لیے ووٹ کیا" + delete: + confirm: + one: "کیا آپ واقعی اُس پوسٹ کو حذف کرنا چاہتے ہیں؟" + other: "کیا آپ واقعی ان {{count}} پوسٹس کو حذف کرنا چاہتے ہیں؟" + merge: + confirm: + one: "کیا آپ واقعی ان پوسٹس کو ضم کرنا چاہتے ہیں؟" + other: "کیا آپ واقعی ان {{count}} پوسٹس کو ضم کرنا چاہتے ہیں؟" revisions: controls: first: "پہلی رَوِیژن" @@ -1629,6 +1993,7 @@ ur: title: "ای میل کے متن کا html حصہ دکھائیں" button: 'HTML' category: + can: 'کر سکتا ہے… ' none: '(کوئی زمرہ نہیں)' all: 'تمام زُمرَہ جات' choose: 'ایک زمرہ منتخب کریں…' @@ -1639,6 +2004,8 @@ ur: settings: 'سیٹِنگ' topic_template: "ٹاپک ٹَیمپلیٹ" tags: "ٹیگز" + tags_allowed_tags: "اس زُمرہ میں صرف اِن ٹیگز کے استعمال کی اجازت دیں:" + tags_allowed_tag_groups: "اس زُمرہ میں صرف اِن گروپس سے ٹیگز کے استعمال کی اجازت دیں:" tags_placeholder: "(اختیاری) اجازت والے ٹیگز کی فہرست" tag_groups_placeholder: "(اختیاری) اجازت والے ٹیگ گروپس کی فہرست" topic_featured_link_allowed: "اس زمرہ میں نمایاں لنکس کو اجازت دیں" @@ -1673,7 +2040,8 @@ ur: email_in_allow_strangers: "اکاؤنٹس نہ رکھنے والے گمنام صارفین کی طرف سے اِیمیلز کو قبول کریں" email_in_disabled: "ویب سائٹ کی سیٹِنگ میں اِیمیل کے ذریعے نئے ٹاپک پوسٹ کرنا غیر فعال کیا ہوا ہے۔ اِیمیل کے ذریعے نئے ٹاپک شائع کرنے کو چالو کرنے کے لئے،" email_in_disabled_click: 'سیٹِنگ میں "اِیمیل اِن" فعال کریں۔' - suppress_from_homepage: "ہَوم پیج سے اِس زمرہ کو دبائیں۔" + mailinglist_mirror: "زُمرہ میلنگ فہرست کا عکس ہے" + suppress_from_latest: "تازہ ترین ٹاپکس سے زُمرہ کو دبائیں۔" show_subcategory_list: "اس زمرہ میں ذیلی زمرہ جات کی فہرست ٹاپکس سے مندرجہ بالا دکھائیں۔" num_featured_topics: "زمرہ کے صفحے پر دکھائے گئے ٹاپکس کی تعداد:" subcategory_num_featured_topics: "بالائی زمرہ کے صفحے پر دکھائے گئے نمایاں ٹاپکس کی تعداد:" @@ -1770,8 +2138,8 @@ ur: post_links: about: "اِس پوسٹ کے لئے مزید لنکس دکھائیں" title: - one: "1 اور" - other: "٪{count} اور" + one: "1 مزید" + other: "%{count}مزید " topic_statuses: warning: help: "یہ ایک آفیشل انتباہ ہے۔" @@ -1829,8 +2197,8 @@ ur: not_available: "دستیاب نہیں ہے!" categories_list: "زمرہ جات فہرست" filters: - with_topics: "٪{فلٹر} ٹاپک" - with_category: "٪{فلٹر} ٪{category} ٹاپک" + with_topics: "%{filter} ٹاپک" + with_category: "%{filter} %{category} ٹاپک" latest: title: "تازہ ترین" title_with_count: @@ -1853,7 +2221,7 @@ ur: unread: title: "بغیر پڑھے" title_with_count: - one: "بغیر پڑھے (1)" + one: "بغیر پڑھا (1)" other: "بغیر پڑھے ({{count}})" help: " ٹاپک جن پر فی الحال آپ نے نظر رکھی ہوئی ہے یا بغیر پڑھی پوسٹس کے ساتھ ٹریک کر رہے ہیں" lower_title_with_count: @@ -1866,7 +2234,7 @@ ur: lower_title: "نئے" title: "نیا" title_with_count: - one: "نئے (1)" + one: "نیا (1)" other: "نئے ({{count}})" help: "پچھلے چند دنوں میں بنائے گئے ٹاپک" posted: @@ -1937,10 +2305,14 @@ ur: hamburger_menu: '= ہیمبرگر مینو کھولیں' user_profile_menu: 'p صارف مینو کھولیں' show_incoming_updated_topics: 'اَپ ڈیٹ ہوے ٹاپک دکھائیں' + search: '/یا ctrl+alt+fسرچ ' help: '? کیبورڈ مدد کھولیں' dismiss_new_posts: 'x، r برخاست کریں نئے/پوسٹس' dismiss_topics: 'x، t ٹاپک برخاست کریں' log_out: 'shift+z shift+z لاگ آوٹ' + composing: + title: 'کمپوز کرنا' + return: 'shift+c کمپوزر پر واپس جائیں' actions: title: 'عمل' bookmark_topic: 'f بُک مارک ٹاپک ٹوگل کریں' @@ -1964,21 +2336,24 @@ ur: badges: earned_n_times: one: "یہ بَیج 1 دفعہ حاصل کیا" - other: "یہ بَیج ٪{count} دفعہ حاصل کیا" - granted_on: "عطا کیا گیا ٪{date}" - others_count: "دوسرے جن کے پاس یہ بَیج ہے (٪{count})" + other: "یہ بَیج %{count} مرتبہ حاصل کیا" + granted_on: "عطا کیا گیا %{date}" + others_count: "دوسرے جن کے پاس یہ بَیج ہے (%{count})" title: بَیج + allow_title: "آپ اِس بَیج کو عنوان کے طور پر استعمال کر سکتے ہیں" + multiple_grant: "آپ اِس کو ایک سے زیادہ مرتبہ حاصل کر سکتے ہیں" badge_count: one: "1 بَیج" other: "%{count} بَیج" more_badges: - one: "+1 اور" - other: "+٪{count} اور" + one: "+1 مزید" + other: "+%{count} مزید" granted: one: "1 عطا کیا گیا" other: "%{count} عطا کیے گئے" select_badge_for_title: اپنے عنوان کے طور پر استعمال کرنے کے لئے ایک بیج منتخب کریں - none: "(none)" + none: "(کوئی نہیں)" + successfully_granted: "%{username}کو کامیابی سے %{badge} عطا کیا" badge_grouping: getting_started: name: شروعات @@ -2001,11 +2376,17 @@ ur:

    tagging: all_tags: "تمام ٹیگز" + other_tags: "دیگر ٹیگز" selector_all_tags: "تمام ٹیگز" selector_no_tags: "کوئی ٹیگ نہیں" changed: "تبدیل کیے گئے ٹیگ:" tags: "ٹیگز" + choose_for_topic: "اختیاری ٹیگز" delete_tag: "ٹیگ حذف کریں" + delete_confirm: + one: "کیا آپ واقعی اس ٹیگ کو حذف اور 1 ٹاپک، جس کو یہ آسائین ہواوا ہے، سے ہٹا دینا چاہتے ہیں؟" + other: "کیا آپ واقعی اس ٹیگ کو حذف اور {{count}} ٹاپکس، جن کو یہ آسائین ہواوا ہے، سے ہٹا دینا چاہتے ہیں؟" + delete_confirm_no_topics: "کیا آپ واقعی اِس ٹَیگ کو حذف کرنا چاہتے ہیں؟" rename_tag: "ٹیگ کا نام تبدیل کریں" rename_instructions: "ٹیگ کے لیے نئے نام کا انتخاب کریں:" sort_by: "ترتیب بہ:" @@ -2014,26 +2395,26 @@ ur: manage_groups: "ٹیگ گروپس کا انتظام کریں" manage_groups_description: "ٹیگز منظم کرنے کے گروپوں کی وضاحت کریں" filters: - without_category: "٪{فلٹر} ٪{ٹیگ} ٹاپکس" - with_category: "٪{فلٹر} ٪{ٹیگ} ٹاپکس ٪{category} میں" - untagged_without_category: "٪{فلٹر} غیر ٹیگ شدہ ٹاپک" - untagged_with_category: "٪{فلٹر} غیر ٹیگ شدہ ٹاپک ٪{category} میں" + without_category: "%{filter} %{tag} ٹاپکس" + with_category: "%{filter} %{tag} ٹاپکس %{category} میں" + untagged_without_category: "%{filter} غیر ٹیگ شدہ ٹاپک" + untagged_with_category: "%{filter} غیر ٹیگ شدہ ٹاپک %{category} میں" notifications: watching: title: "نظر رکھی ہوئی ہے" - description: "آپ خود کار طریقے سے اِس ٹیگ میں موجود تمام ٹاپکس پر نظر رکھیں گے۔ تمام نئی پوسٹ اور ٹاپکس کے بارے میں مطلع کیا جائے گا، اور نئی پوسٹس کی گنتی بھی ٹاپک کے ساتھ دکھائی جائے گی۔" + description: "آپ خود کار طریقے سے اِس ٹیگ والے تمام ٹاپکس پر نظر رکھیں گے۔ تمام نئی پوسٹ اور ٹاپکس کے بارے میں مطلع کیا جائے گا، اور نئی پوسٹس کی گنتی بھی ٹاپک کے ساتھ دکھائی جائے گی۔" watching_first_post: title: "پہلی پوسٹ پر نظر رکھی ہوئی ہے" - description: "آپ کو اِس ٹیگ میں ہر نئے ٹاپک کی صرف پہلی پوسٹ کے بارے میں مطلع کیا جائے گا۔" + description: "آپ کو اِس ٹیگ والے ہر نئے ٹاپک کی صرف پہلی پوسٹ کے بارے میں مطلع کیا جائے گا۔" tracking: title: "ٹریک کیا جا رہا" - description: "آپ خود کار طریقے سے اِس ٹیگ میں موجود تمام ٹاپکس کو ٹریک کریں گے۔ بغیر پڑھی اور نئی پوسٹس کی گنتی ٹاپک کے ساتھ دکھائی جائے گی۔" + description: "آپ خود کار طریقے سے اِس ٹیگ کے ساتھ موجود تمام ٹاپکس کو ٹریک کریں گے۔ بغیر پڑھی اور نئی پوسٹس کی گنتی ٹاپک کے ساتھ دکھائی جائے گی۔" regular: title: "معمولی" description: "اگر کوئی آپ کا @نام زکر کرتا ہے یا آپ کی پوسٹ پر جواب دیتا ہے تو آپ کو مطلع کر دیا جائے گا۔" muted: title: "خاموش کِیا ہوا" - description: "آپ کو اِس ٹیگ میں موجود نئے ٹاپکس کی کسی بھی چیز کے بارے میں مطلع نہیں کیا جائے گا، اور یہ تازہ ترین میں بھی نظر نہیں آئیں گے۔" + description: "آپ کو اِس ٹیگ کے ساتھ موجود نئے ٹاپکس کی کسی بھی چیز کے بارے میں مطلع نہیں کیا جائے گا، اور یہ تازہ ترین میں بھی نظر نہیں آئیں گے۔" groups: title: "ٹیگ گروپس" about: "زیادہ آسانی سے ٹیگز کو منظم کرنے کے لیے اُن کو گروپوں میں شامل کریں۔" @@ -2100,11 +2481,12 @@ ur: no_problems: "کوئی مسائل نہیں پائے گئے۔" moderators: 'ماڈریٹرز:' admins: 'ایڈمن:' + silenced: 'خاموش کیو ہوئے: ' suspended: 'معطل کردہ:' private_messages_short: "پغم" private_messages_title: "پیغامات" mobile_title: "موبائل" - space_free: "{{size}} فرِی" + space_free: "{{size}} فارغ" uploads: "اَپ لوڈز" backups: "بیک اَپس" traffic_short: "ٹریفک" @@ -2132,12 +2514,29 @@ ur: by: "تک" flags: title: "فلَیگز" + active_posts: "فلَیگ کی گئی پوسٹس" + old_posts: "پرانی فلَیگ کی گئی پوسٹس" + topics: "فلَیگ کیے گئے ٹاپک" + moderation_history: "ماڈرَیشن ہِسٹری" agree: "اتفاق کریں" agree_title: "اِس فلَیگ کے درست اور صحیح ہونے کی تصدیق کریں" - defer_flag_title: "اس فلَیگ کو ہٹا دیں؛ اِسے اِس وقت کسی کارروائی کی ضرورت نہیں۔" + agree_flag_hide_post: "پوسٹ چھپائیں" + agree_flag_hide_post_title: "اِس پوسٹ کو چھپائیں اور خود کار طریقے سے صارف کو اِس میں ترمیم کرنے کیلئے زور ڈالنے کیلئے پیغام بھیجیں۔" + agree_flag_restore_post: "اتفاق کریں اور پوسٹ بحال کریں" + agree_flag_restore_post_title: "پوسٹ کو بحال کریں تاکہ تمام صارفین اِسے دیکھ سکیں۔" + agree_flag_suspend: "صارف معطل کریں" + agree_flag_suspend_title: "فلَیگ کے ساتھ اتفاق کریں اور صارف معطل کریں۔" + agree_flag_silence: "صارف خاموش کریں" + agree_flag_silence_title: "فلَیگ کے ساتھ اتفاق کریں اور صارف خاموش کریں۔" + agree_flag: "پوسٹ برقرار رکھیں" + agree_flag_title: "فلَیگ کے ساتھ اتفاق کریں اور پوسٹ کو ویسا ہی رہنے دیں۔" + ignore_flag: "نظر انداز کریں" + ignore_flag_title: "اس فلَیگ کو ہٹا دیں؛ اِسے اِس وقت کسی کارروائی کی ضرورت نہیں۔" delete: "حذف کریں" delete_title: "یہ فلَیگ جس پوسٹ سے متعلق ہے اُسے حذف کریں۔" + delete_post_defer_flag: "پوسٹ حذف اور فلَیگ نظر انداز کریں" delete_post_defer_flag_title: "پوسٹ حذف کریں؛ اگر پہلی پوسٹ، تو ٹاپک بھی ساتھ حذف کریں" + delete_post_agree_flag: "پوسٹ حذف اور فلَیگ کے ساتھ اتفاق کریں" delete_post_agree_flag_title: "پوسٹ حذف کریں؛ اگر پہلی پوسٹ، تو ٹاپک بھی ساتھ حذف کریں" delete_flag_modal_title: "حذف کریں اور..." delete_spammer: "سپیم کرنے والے کو حذف کریں" @@ -2149,19 +2548,37 @@ ur: clear_topic_flags: "ہو گیا" clear_topic_flags_title: "اِس ٹاپک کی تفتیش ہو چکی ہے اور مسائل حل کر دیے گئے ہیں۔ فلَیگز کو ہٹانے کیلئے \"ہو گیا\" پر کلک کریں۔" more: "(مزید جوابات...)" + suspend_user: "صارف معطل کریں" + suspend_user_title: "اِس پوسٹ کی وجہ سے صارف معطل کریں" dispositions: agreed: "اتفاق کیا" disagreed: "اختلاف کیا" + deferred: "نظر انداز کیا ہوا" flagged_by: "کی طرف سے فلَیگ کیا گیا" resolved_by: "کی طرف سے حل کیا گیا" took_action: "کارروائی کی" system: "سِسٹَم" error: "کچھ غلط ہو گیا" reply_message: "جواب" + no_results: "کوئی فلَیگ کی گئی پوسٹس موجود نہیں۔" topic_flagged: "اِس ٹاپک کو فلَیگ کر دیا گیا ہے۔" + show_full: "مکمل پوسٹ دکھائیں" visit_topic: "کارروائی کرنے کیلئے ٹاپک کو وزٹ کریں" was_edited: "پہلے فلَیگ کے بعد پوسٹ میں ترمیم ہوئی تھی" previous_flags_count: "اس پوسٹ کو پہلے ہی سے {{count}} دفعہ فلَیگ کیا جا چکا ہے۔" + show_details: "فلَیگ کی تفصیلات دکھائیں" + details: "تفصیلات" + flagged_topics: + topic: "ٹاپک" + type: "قِسم" + users: "صارفین" + last_flagged: "آخری دفعہ فلَیگ کیا گیا" + short_names: + off_topic: "موضوع سے ہٹ کر" + inappropriate: "نامناسب" + spam: "سپَیم" + notify_user: "اپنی مرضی کا" + notify_moderators: "اپنی مرضی کا" groups: primary: "پرائمری گروپ" no_primary: "(کوئی پرائمری گروپ نہیں)" @@ -2169,17 +2586,17 @@ ur: edit: "گروپس میں ترمیم کریں" refresh: "رِیفریش" new: "نیا" - selector_placeholder: "صارف نام درج کریں" about: "یہاں اپنے گروپ کی رکنیت کو اور ناموں میں ترمیم کریں" group_members: "گروپ کے اراکین" delete: "حذف کریں" delete_confirm: "اِس گروپ کو حذف کریں؟" delete_failed: "گروپ کو حذف کرنے میں ناکام۔ اگر یہ ایک خودکار گروپ ہے تو اسے ختم نہیں کیا جا سکتا۔" - delete_owner_confirm: "'٪{username}' کے لیے مالک کے استحقاق کو ہٹائیں؟" + delete_owner_confirm: "'%{username}' کے لیے مالک کے استحقاق کو ہٹائیں؟" add: "شامل کریں" add_members: "اراکین شامل کریں" custom: "اپنی مرضی کا" bulk_complete: "صارفین کو گروپ میں شامل کر لیا گیا ہے۔" + bulk_complete_users_not_added: "یہ صارفین شامل نہیں ہو سکے (اس بات کو یقینی بنائیں کہ ان کا اکاؤنٹ موجود ہے):" bulk: "گروپ میں کئی شامل کریں" bulk_paste: "صارف ناموں اور یا اِیمیلز کی فہرست پَیسٹ کریں، فی سطر ایک:" bulk_select: "(ایک گروپ منتخب کریں)" @@ -2229,6 +2646,7 @@ ur: event_type_missing: "آپ کو کم از کم ایک واقعہ کی قسم قائم کرنا ضروری ہے۔" content_type: "کونٹنٹ کی قسم" secret: "سیکرٹ" + event_chooser: "کونسے واقعات اِس وَیب ھُوک٘ کو متحرک کریں؟" wildcard_event: "مجھے سب کچھ بھیجیں۔" individual_event: "انفرادی واقعات کو منتخب کریں۔" verify_certificate: "پَیلوڈ یو.آر.ایل. کا TLS سرٹیفکیٹ چیک کریں" @@ -2247,6 +2665,7 @@ ur: details: "جب ایک نیا جواب، ترمیم، حذف یا رِِیکور کیا جائے۔" user_event: name: "صارف واقعہ" + details: "جب ایک صارف لاگ اِن، لاگ آؤٹ، بنایا، منظور یا اَپ ڈیٹ کیا جاتا ہے۔" delivery_status: title: "موصول ہونے کی آگاہی" inactive: "غیر فعال" @@ -2311,7 +2730,7 @@ ur: label: "اَپ لوڈ" title: "اِس صورتحال کیلئے ایک بیک اَپ اَپ لوڈ کریں" uploading: "اَپ لوڈ کیا جا رہا ہے..." - success: "'{{filename}}' کو کامیابی سے اَپ لوڈ کر دیا گیا ہے۔" + success: "'{{filename}}' کامیابی سے اَپ لوڈ کردی گئی ہے۔ فائل پر اب کام جا ری ہے اور فہرست میں ظاہر ہونے کیلئے ایک منٹ تک لگ سکتا ہے۔" error: "اَپ لوڈ کرتے وقت ایک خرابی کا سامنا کرنا پڑا '{{filename}}': {{message}}" operations: is_running: "ایک کارروائی اِس وقت چل رہی ہے..." @@ -2360,15 +2779,21 @@ ur: title: "مرضی کے مطابق بنائیں" long_title: "ویب سائٹ کو اپنی مرضی کے حساب سے بنانے کیلئے اختیارات" preview: "پیشگی دیکھیں" + explain_preview: "اِس تِھیم کے ساتھ سائٹ ملاحظہ کریں" save: "محفوظ کریں" new: "نیا" new_style: "نیا سٹائل" import: "اِمپورٹ" delete: "حذف کریں" + delete_confirm: "اس تِھیم کو حذف کریں؟" about: "سائٹ پر CSS سٹائل شیٹس اور HTML ہیڈرز میں ترمیم کریں۔ شروع کرنے کیلئے ایک اِصلاح شامل کریں۔" color: "رنگ" opacity: "دھندلاپن" copy: "کاپی" + copy_to_clipboard: "کلِپ بورڈ میں کاپی کریں" + copied_to_clipboard: "کلِپ بورڈ میں کاپی کر لیا گیا" + copy_to_clipboard_error: "کلِپ بورڈ میں ڈیٹا کاپی کرنے پر خرابی کا سامنا کر نا پرا" + theme_owner: "ناقابلِ ترمیم، ملکیت:" email_templates: title: "اِیمیل ٹَیمپلیٹ" subject: "موضوع" @@ -2377,9 +2802,85 @@ ur: none_selected: "ترمیم شروع کرنے کیلئے ایک اِیمیل ٹَیمپلیٹ منتخب کریں۔" revert: "تبدیلیاں لوٹائیں" revert_confirm: "کیا آپ واقعی تبدیلیاں لوٹانا چاہتے ہیں؟" + theme: + import_theme: "تِھیم درآمد کریں" + customize_desc: "مرضی کے مطابق بنائیں:" + title: "تِھیمز" + long_title: "اپنی سائٹ کے رنگ، سی ایس ایس اور ایچ ٹی ایم ایل میں ترمیم کریں" + edit: "ترمیم" + edit_confirm: "یہ ایک ریمَوٹ تِھیم ہے، اگر آپ سی ایس ایس / ایچ ٹی ایم ایل میں ترمیم کرتے ہیں تو اگلی دفعہ تِھیم اَپ ڈیٹ ہونے پر یہ تبدیلیاں مٹا دی جائیں گی۔" + common: "عام" + desktop: "ڈیسک ٹاپ" + mobile: "موبائل" + settings: "ترتیبات" + preview: "پیشگی دیکھیں" + is_default: "تِھیم ڈیفالٹ کے طور پر فعال ہے" + user_selectable: "صارفین تِھیم کو منتخب کرسکتے ہیں" + color_scheme: "رنگ سکیم" + color_scheme_select: "تِھیم میں استعمال ہونے والے رنگ منتخب کریں" + custom_sections: "اپنی مرضی کے حصے:" + theme_components: "تِھیم کے اجزاء" + uploads: "اَپ لوڈز" + no_uploads: "آپ اپنی تِھیم کے ساتھ منسلک اثاثے، جیسے کہ فونٹ اور تصاویر، اَپ لوڈ کر سکتے ہیں" + add_upload: "اَپ لوڈ شامل کریں" + upload_file_tip: "اَپ لوڈ کرنے کے لئے اثاثہ منتخب کریں (png، woff2، وغیرہ...)" + variable_name: "SCSS وار نام:" + variable_name_invalid: "غلط وَیری اَیبل نام۔ صرف حرف و نمبر کی اجازت ہے۔ ایک حرف کے ساتھ شروع ہونا لاذمی ہے۔" + upload: "اَپ لوڈ" + child_themes_check: "تِھیم میں دوسری ماتحت تِھیمز شامل ہیں" + css_html: "اپنی مرضی کے مطابق CSS/HTML" + edit_css_html: "CSS/HTML ترمیم کریں" + edit_css_html_help: "آپ نے کوئی بھی CSS یا HTML ترمیم نہیں کیا ہے" + delete_upload_confirm: "اِس اَپ لوڈ کو حذف کریں؟ (تِھیم CSS کام کرنا بند کر سکتی ہے!)" + import_web_tip: "رِیپَوزِٹَری میں موجود تِھیم" + import_file_tip: ".dcstyle.json فائل میں موجود تِھیم" + is_private: "تھِیم ایک ذاتی گِٹ رِیپَوزِٹَری میں ہے" + public_key: "رِیپَوزِٹَری کو درج ذیل پبلک کلید ایکسَیس فراہم کریں:" + about_theme: "تِھیم کے بارے میں" + license: "لائسنس" + component_of: "تِھیم ایک جزو ہے:" + update_to_latest: "تازہ ترین ورژن تک اِپ ڈَیٹ کریں" + check_for_updates: "اپ ڈیٹ کے لیے چیک کریں" + updating: "اَپ ڈَیٹ کیا جا رہا ہے..." + up_to_date: "تِھیم اَپ ڈَیٹ ہے، آخری دفعہ چَیک کیا:" + add: "شامل کریں" + theme_settings: "تھیم کی ترتیبات" + no_settings: "اِس تھیم کی کوئی ترتیبات نہیں ہیں۔" + commits_behind: + one: "تِھیم 1 کَمِٹ پیچھے ہے!" + other: "تِھیم {{count}} کَمِٹس پیچھے ہے!" + scss: + text: "CSS" + title: "اپنی مرضی کے مطابق CSS درج کریں، ہم تمام درست CSS اور SCSS سٹائلز قبول کرتے ہیں" + header: + text: "ہَیڈر" + title: "سائٹ ہَیڈر کے اوپر ظاہر کرنے کے لئے HTML درج کریں" + after_header: + text: "ہَیڈر کے بعد" + title: "تمام صفحات پر ہَیڈر کے بعد ظاہر کرنے کے لئے HTML درج کریں" + footer: + text: "فوٹر" + title: "صفحہ کے فوٹر پر ظاہر کرنے کے لئے HTML درج کریں" + embedded_scss: + text: "اَیمبَیڈ کیا ہوا CSS" + title: "تبصرے کے اَیمبَیڈ شدہ ورژن کے ساتھ فراہم کیے جانے والا اپنی مرضی کا CSS درج کریں" + head_tag: + text: "" + title: "HTML جو ٹیگ سے پہلے داخل کی جائے گی" + body_tag: + text: "" + title: "HTML جو ٹیگ سے پہلے داخل کی جائے گی" + yaml: + text: "YAML" + title: "تھیم ترتیبات کی وضاحت YAML فارمَیٹ میں کریں" colors: + select_base: + title: "بَیس رنگ سکیم منتخب کریں" + description: "بَیس سکیم:" title: "رنگ" + edit: "رنگ سکیمیں ترمیم کریں" long_title: "رنگ سکیمیں" + about: "اپنی تِھیمز کے استعمال کردہ رنگوں میں ترمیم کریں۔ شروع کرنے کے لئے ایک نئی سکیم بنائیں۔" new_name: "نئی رنگ سکیم" copy_name_prefix: "نقل از" delete_confirm: "اِس رنگ سکیم کو حذف کریں؟" @@ -2423,7 +2924,7 @@ ur: templates: "ٹَیمپلیٹ" preview_digest: "ڈائجسٹ کا مشاہدہ کریں" sending_test: "ٹیسٹ اِیمیل بھیجی جا رہی ہے..." - error: "تکنیکی خرابی - ٪{server_error}" + error: "خرابی - %{server_error}" test_error: "ٹیسٹ اِیمیل بھیجنے میں ایک مسئلہ پیش آیا۔ اپنی میل سیٹِنگ دوبارہ چیک کریں، تصدیق کر لیں کہ آپ کا ہَوسٹ میل کنکشنوں کو بلاک تو نہیں کر رہا، اور دوبارہ کوشش کریں۔" sent: "بھیج دی گئی" skipped: "چھوڑا گیا" @@ -2480,6 +2981,15 @@ ur: type_placeholder: "ڈائجسٹ، سائن اَپ..." reply_key_placeholder: "جواب کِی" skipped_reason_placeholder: "وجہ" + moderation_history: + performed_by: "کی طرف سے عمل کیا کیا" + no_results: "کوئی ماڈرَیشَن ہسٹری موجود نہیں ہے۔" + actions: + delete_user: "صارف حذف کر دیا گیا" + suspend_user: "صارف معطل کر دیا گیا" + silence_user: "صارف خاموش کر دیا گیا" + delete_post: "پوسٹ حذف کر دی گئی" + delete_topic: "ٹاپک حذف کر دیا گیا" logs: title: "لاگز" action: "عمل" @@ -2497,6 +3007,8 @@ ur: block: "بلاک کریں" do_nothing: "کچھ نہ کریں" staff_actions: + all: "تمام" + filter: "فِلٹر:" title: "سٹاف عوامل" clear_filters: "سب کچھ دکھائیں" staff_user: "سٹاف یوزر" @@ -2517,6 +3029,8 @@ ur: change_trust_level: "ٹرسٹ لَیول تبدیل کریں" change_username: "صارف نام تبدیل کریں" change_site_setting: "ویب سائٹ کی سیٹِنگ تبدیل کریں" + change_theme: "تِھیم تبدیل کریں" + delete_theme: "تِھیم حذف کریں" change_site_text: "سائٹ ٹَیکسٹ تبدیل کریں" suspend_user: "صارف معطل کریں" unsuspend_user: "صارف کی معطلی ختم کریں" @@ -2524,6 +3038,7 @@ ur: revoke_badge: "بَیج منسوخ کریں" check_email: "اِیمیل چیک کریں" delete_topic: "ٹاپک حذف کریں" + recover_topic: "ٹاپک غیر حذف کریں" delete_post: "پوسٹ حذف کریں" impersonate: "نقالی" anonymize_user: "صارف گمنام بنائیں" @@ -2531,6 +3046,8 @@ ur: change_category_settings: "زمرہ جات کی سیٹِنگ تبدیل کریں" delete_category: "زمرہ حذف کریں" create_category: "زمرہ بنائیں" + silence_user: "صارف خاموش کریں" + unsilence_user: "صارف کی خاموشی ختم کریں" grant_admin: "اَیڈمِن عطا کریں" revoke_admin: "اَیڈمِن منسوخ کریں" grant_moderation: "ماڈرَیشَن عطا کریں" @@ -2546,6 +3063,13 @@ ur: change_readonly_mode: "صرف پڑھنے کا مَوڈ تبدیل کریں" backup_download: "بیک اَپ ڈاؤن لوڈ کریں" backup_destroy: "بیک اَپ تباہ کریں" + reviewed_post: "پوسٹ کا جائزہ لیا" + custom_staff: "پلَگ اِن کیلئے اپنی مرضی کا عمل" + post_locked: "پوسٹ لاک کر دی گئی" + post_edit: "پوسٹ ترمیم" + post_unlocked: "پوسٹ کھول دی گئی" + check_personal_message: "ذاتی پیغام چیک کریں" + disabled_second_factor: "دو فیکٹر توثیق غیر فعال کریں" screened_emails: title: "سکرین کی گئی اِیمیلز" description: "جب کوئی نیا اکاؤنٹ بنانے کی کوشش کرے گا، تو مندرجہ ذیل اِیمیل ایڈریسوں کو چیک کیا جائے گا اور رجسٹریشن بلاک، یا کوئی اور کارروائی کی جائے گی۔" @@ -2560,9 +3084,9 @@ ur: screened_ips: title: "سکرین کیے گئے IP" description: 'IP ایڈریس جن پر نظر رکھی ہوئی ہے۔ IP ایڈریسوں کو وائٹ لِسٹ کرنے کیلئے "اجازت دیں" کو استعمال کریں۔' - delete_confirm: "کیا آپ واقعی ٪{IP_ADDRESS} کیلئے اصول کو ہٹانا چاہتے ہیں؟" + delete_confirm: "کیا آپ واقعی %{ip_address} کیلئے اصول کو ہٹانا چاہتے ہیں؟" roll_up_confirm: "کیا آپ واقعی عام سکرین کیے گئے IP ایڈریسوں کو subnets میں رول اَپ کرنا چاہتے ہیں؟" - rolled_up_some_subnets: "کامیابی کے ساتھ IP بین اندراجات کو اِن subnets میں رول اَپ کر دیا گیا: ٪{subnets}۔" + rolled_up_some_subnets: "کامیابی کے ساتھ IP بین اندراجات کو اِن subnets میں رول اَپ کر دیا گیا: %{subnets}۔" rolled_up_no_subnet: "رول اَپ کرنے کیلئے کچھ بھی نہیں تھا۔" actions: block: "بلاک کریں" @@ -2576,8 +3100,47 @@ ur: roll_up: text: "رول اَپ" title: "نئے subnet بین اندراجات بناتا ہے اگر کم از کم 'min_ban_entries_for_roll_up' اندراجات موجود ہوں۔" + search_logs: + title: "لاگز سرچ کریں" + term: "اصطلاح" + searches: "سرچز" + click_through: "کلِک تھرُو" + unique: "منفرد" + unique_title: "سرچ کرنے والے منفرد صارفین" + types: + all_search_types: "تمام سرچ ٹاپکس" + header: "ہَیڈر" + full_page: "مکمل پیج" + click_through_only: "تمام (صرف کلِک تھرُو)" + header_search_results: "ہیڈر کے سرچ نتائج" logster: title: "خرابیوں کے لاگز" + watched_words: + title: "نظر رکھے ہوئے الفاظ" + search: "سرچ" + clear_filter: "صاف کریں" + show_words: "الفاظ دکھائیں" + word_count: + one: "1 لفظ" + other: "%{count}الفاظ " + actions: + block: 'بلاک کریں' + censor: 'سَینسَر' + require_approval: 'منظوری کی ضرورت ہے' + flag: 'فلَیگ' + action_descriptions: + block: 'اِن الفاظ پر مشتمل پوسٹس شائع ہونے سے روکیں۔ جب صارف اپنی پوسٹ شائع کرنے کی کوشش کرے گا تو اُسے ایک خرابی کا پیغام دکھایا جائے گا۔' + censor: 'اِن الفاظ پر مشتمل پوسٹس کوشائع ہونے دیں، لیکن سینسر کیے گئے الفاظ کو چھپانے والے حروف کے ساتھ تبدیل کریں۔' + require_approval: 'اِن الفاظ پر مشتمل پوسٹس کے نظر آنے سے پہلے اسٹاف کی طرف سے منظوری کی ضرورت ہوگی۔' + flag: 'اِن الفاظ پر مشتمل پوسٹس کوشائع ہونے دیں، لیکن ان کو غیر مناسب کے طور پر فلَیگ کریں تاکہ منتظمین ان کا جائزہ لے سکیں۔' + form: + label: 'نیا لفظ:' + placeholder: 'مکمل لفظ یا * وائلڈ کارڈ کے طور پر' + placeholder_regexp: "رَیگولر اَیکسپرَیشَن" + add: 'شامل کریں' + success: 'کامیابی' + upload: "اَپ لوڈ" + upload_successful: "اَپ لوڈ کامیاب ہوا۔ الفاظ شامل کردیے گئے ہیں۔" impersonate: title: "نقالی" help: "ڈِیبَگ مقاصد کیلئے اِس آلے کو استعمال کر کہ ایک صارف کے اکاؤنٹ کی نقالی کریں۔ کام ختم ہونے کے بعد آپ کے ایک دفعہ لاگ آؤٹ کرنا ہو گا۔" @@ -2597,6 +3160,7 @@ ur: pending: "زیرِاِلتوَاء" staff: 'سٹاف' suspended: 'معطل کردہ' + silenced: 'خاموش کیو ہوئے' suspect: 'مشتبہ' approved: "منظورشدہ؟" approved_selected: @@ -2617,14 +3181,15 @@ ur: staff: "سٹاف" admins: 'ایڈمِن صارفین' moderators: 'ماڈریٹرز' + silenced: 'خاموش کردہ صارفین' suspended: 'معطل کردہ صارفین' suspect: 'مشتبہ صارفین' reject_successful: one: "کامیابی کے ساتھ 1 صارف کو مسترد کیا۔" - other: "کامیابی کے ساتھ ٪{count} صارفین کو مسترد کیا۔" + other: "کامیابی کے ساتھ %{count} صارفین کو مسترد کیا۔" reject_failures: one: "1 صارف کو مسترد کرنے میں ناکامی۔" - other: "٪{count} صارفین کو مسترد کرنے میں ناکامی۔" + other: "%{count} صارفین کو مسترد کرنے میں ناکامی۔" not_verified: "غیر تصدیق شدہ" check_email: title: "اِس صارف کا اِیمیل ایڈریس ظاہر کریں" @@ -2634,10 +3199,31 @@ ur: unsuspend_failed: "اِس صارف کی معطلی ختم کرتے ہوے کچھ غلط ہو گیا {{error}}" suspend_duration: "صارف کب تک معطل رہے گا؟" suspend_reason_label: "آپ معطل کیوں کر رہے ہیں؟ یہ ٹَیکسٹ اِس صارف کے پروفائل پیج پر ہر کسی کے لیے ظاہر ہو گا، اور جب صارف لاگ اِن کرنے کی کوشش کریں گے تو اُنہیں دکھایا جائے گا۔ اِسے مختصر رکھیں۔" + suspend_reason_hidden_label: "آپ کیوں معطل کر رہے ہیں؟ صارف جب لاگ اِن کرنے کی کوشش کرے گا تو اُسے یہ ٹیکسٹ دکھایا جائے گا۔ اِسے مختصر رکھیں۔" suspend_reason: "وجہ" + suspend_reason_placeholder: "وجہ معطلی" + suspend_message: "اِی مَیل پیغام" + suspend_message_placeholder: "اختیاریطور پر معطلی کے بارے میں مزید معلومات فراہم کریں اور یہ صارف کو ای میل کر دی جائیں گی۔" suspended_by: "کی طرف سے معطل کیا گیا" + silence_reason: "وجہ" + silenced_by: "جس نے خاموش کیا" + silence_modal_title: "صارف خاموش کریں" + silence_duration: "صارف کب تک خاموش رہے گا؟" + silence_reason_label: "آپ اِس صارف کو کیوں خاموش کر رہے ہیں؟" + silence_reason_placeholder: "خاموش کردینے کی وجہ" + silence_message: "اِی مَیل پیغام" + silence_message_placeholder: "(ڈیفالٹ پیغام بھیجنے کیلئے خالی چھوڑ دیں)" + suspended_until: "(%{until} تک)" + cant_suspend: "یہ صارف معطل نہیں کیا جا سکتا۔" delete_all_posts: "تمام پوسٹس حذف کریں" + penalty_post_actions: "آپ اِس مُنسلِک پوسٹ کے ساتھ کیا کرنا چاہیں گے؟" + penalty_post_delete: "پوسٹ حذف کریں" + penalty_post_edit: " پوسٹ ترمیم کریں" + penalty_post_none: "کچھ نہ کریں" delete_all_posts_confirm_MF: "آپ {POSTS، جمع، ایک {1 پوسٹ} دیگر {# posts}} اور {TOPICS، جمع، ایک {1 ٹاپک} دیگر {# topics}} حذف کرنے والے ہیں۔ کیا آپ کو یقین ہے؟" + silence: "خاموش کریں" + unsilence: "خاموشی ختم کریں" + silenced: "خاموش کیا ہوا؟" moderator: "ماڈریٹر؟" admin: "ایڈمن؟" suspended: "معطل کردہ؟" @@ -2653,10 +3239,14 @@ ur: logged_out: "صارف کو تمام ڈِیوائیسِز سے لاگ آؤٹ کر دیا گیا" revoke_admin: 'اَیڈمِن منسوخ کریں' grant_admin: 'اَیڈمِن عطا کریں' + grant_admin_confirm: "نئے ایڈمِنِسٹریٹر کی توثیق کیلئے ہم نے آپ کو ایک ای میل بھیجی ہے۔ براہ مہربانی اسے کھولیں اور ہدایات پر عمل کریں۔" revoke_moderation: 'ماڈرَیشَن منسوخ کریں' grant_moderation: 'ماڈرَیشَن عطا کریں' unsuspend: 'معطلی ختم کریں' suspend: 'معطل کریں' + show_flags_received: "ملے فلَیگز دکھائیں" + flags_received_by: "%{username} کو ملے فلَیگز " + flags_received_none: "اِس صارف کو کوئی فلَیگز نہیں ملے۔" reputation: ساکھ permissions: اجازتیں activity: سرگرمی @@ -2665,6 +3255,7 @@ ur: private_topics_count: نِجی ٹاپک posts_read_count: پڑھی گئیں پوسٹس post_count: بنائی گئیں پوسٹس + second_factor_enabled: دو فیکٹر توثیق فعال کردی گئی topics_entered: دیکھ لیے گئے ٹاپک flags_given_count: فلَیگز دیے گئے flags_received_count: فلَیگز ملے @@ -2683,14 +3274,14 @@ ur: delete_forbidden_because_staff: "اَیڈمن اور ماڈریٹرز حذف نہیں کیے جا سکتے۔" delete_posts_forbidden_because_staff: "اَیڈمن اور ماڈریٹرز کی تمام پوسٹس حذف نہیں کی جا سکتیں۔" delete_forbidden: - one: "اگر صارفین کی پوسٹس موجود ہوں تو اُنہیں حذف نہیں کیا جا سکتا۔ ایک صارف کو حذف کرنے کی کوشش سے پہلے تمام پوسٹس کو حذف کریں۔ (٪{count} دن سے زیادہ پرانی پوسٹس کوحذف نہیں کیا جا سکتا۔)" - other: "اگر صارفین کی پوسٹس موجود ہوں تو اُنہیں حذف نہیں کیا جا سکتا۔ ایک صارف کو حذف کرنے کی کوشش سے پہلے تمام پوسٹس کو حذف کریں۔ (٪{count} دن سے زیادہ پرانی پوسٹس کوحذف نہیں کیا جا سکتا۔)" + one: "اگر صارفین کی پوسٹس موجود ہوں تو اُنہیں حذف نہیں کیا جا سکتا۔ ایک صارف کو حذف کرنے کی کوشش سے پہلے تمام پوسٹس کو حذف کریں۔ (%{count} دن سے زیادہ پرانی پوسٹس کوحذف نہیں کیا جا سکتا۔)" + other: "اگر صارفین کی پوسٹس موجود ہوں تو اُنہیں حذف نہیں کیا جا سکتا۔ ایک صارف کو حذف کرنے کی کوشش سے پہلے تمام پوسٹس کو حذف کریں۔ (%{count} دن سے زیادہ پرانی پوسٹس کوحذف نہیں کیا جا سکتا۔)" cant_delete_all_posts: - one: "تمام پوسٹس کو حذف نہیں کیا جاسکتا۔ کچھ پوسٹس ٪{count} دن پرانی سے زیادہ پرانی ہیں۔(delete_user_max_post_age سیٹِنگ۔)" - other: "تمام پوسٹس کو حذف نہیں کیا جاسکتا۔ کچھ پوسٹس ٪{count} دن پرانی سے زیادہ پرانی ہیں۔(delete_user_max_post_age سیٹِنگ۔)" + one: "تمام پوسٹس کو حذف نہیں کیا جاسکتا۔ کچھ پوسٹس %{count} دن پرانی سے زیادہ پرانی ہیں۔(delete_user_max_post_age سیٹِنگ۔)" + other: "تمام پوسٹس کو حذف نہیں کیا جاسکتا۔ کچھ پوسٹس %{count} دن پرانی سے زیادہ پرانی ہیں۔(delete_user_max_post_age سیٹِنگ۔)" cant_delete_all_too_many_posts: one: "تمام پوسٹس کو حذف نہیں کیا جاسکتا کیونکہ صارف کی 1 سے زیادہ پوسٹس ہیں۔ (delete_all_posts_max)" - other: "تمام پوسٹس کو حذف نہیں کیا جاسکتا کیونکہ صارف کی ٪{count} سے زیادہ پوسٹس ہیں۔ (delete_all_posts_max)" + other: "تمام پوسٹس کو حذف نہیں کیا جاسکتا کیونکہ صارف کی %{count} سے زیادہ پوسٹس ہیں۔ (delete_all_posts_max)" delete_confirm: "کیا آپ واقعی اِس صارف کو حذف کرنا چاہتے ہیں؟ یہ مُستقِل عمل ہے!" delete_and_block: "اِس اِیمیل اور IP ایڈریس کو حذف اور بلاک کریں" delete_dont_block: "صرف حذف کریں" @@ -2698,17 +3289,23 @@ ur: delete_failed: "اُس صارف کو حذف کرنے میں ایک مسئلہ پیش آیا۔ صارف کو حذف کرنے سے پہلے اس بات کا یقین کرلیں کہ تمام پوسٹس حذف کر دی گئی ہیں۔" send_activation_email: "ایکٹیویشن اِیمیل بھیجییں" activation_email_sent: "ایک ایکٹیویشن اِیمیل بھیج دی گئی ہے۔" - send_activation_email_failed: "ایک اور ایکٹیویشن اِیمیل بھیجنے میں ایک مسئلہ پیش آیا۔ ٪{error}" + send_activation_email_failed: "ایک اور ایکٹیویشن اِیمیل بھیجنے میں ایک مسئلہ پیش آیا۔ %{error}" activate: "اکاؤنٹ فعال کریں" activate_failed: "صارف فعال کرنے میں ایک مسئلہ پیش آیا۔" deactivate_account: "اکاؤنٹ غیر فعال کریں" deactivate_failed: "صارف کو غیر فعال کرنے میں ایک مسئلہ پیش آیا۔" + unsilence_failed: 'صارف کی خاموشی ختم کرنے میں ایک مسئلہ پیش آیا۔' + silence_failed: 'صارف کو خاموش کرنے میں ایک مسئلہ پیش آیا۔' + silence_confirm: 'کیا آپ واقعی اِس صارف کو خاموش کرنا چاہتے ہیں؟ وہ کوئی بھی نئے ٹاپک یا پوسٹ نہیں بنا سکیں گے۔' + silence_accept: 'جی ہاں، اِس صارف کو خاموش کریں' bounce_score: "بائونس سکور" reset_bounce_score: label: "رِی سَیٹ" title: "بائونس سکور واپس 0 پر رِی سَیٹ کریں" + visit_profile: "اِس صارف کے پروفائل میں ترمیم کرنے کیلئے اِس کے ترجیحات پَیج پر جائیں" deactivate_explanation: "ایک غیر فعال صارف کو اپنا اِیمیل دوبارہ تصدیق کرنا ضروری ہے۔" suspended_explanation: "کوئی معطل صارف لاگ اِن نہیں کر سکتا۔" + silence_explanation: "ایک خاموش کیا ہوا صارف پوسٹ شائع یا ٹاپک شروع نہیں کر سکتا۔" staged_explanation: "ایک سٹَیجڈ صارف صرف مخصوص ٹاپکس پر اِیمیل کے ذریعے پوسٹ کر سکتا ہے۔" bounce_score_explanation: none: "اس اِیمیل سے حال میں کوئی باؤنسِز موصول نہیں ہوئے۔" @@ -2726,7 +3323,7 @@ ur: title: "ٹرسٹ لَیول 3 کے تقاضے" table_title: one: "پچھلے دن میں" - other: "پچھلے ٪{count} دنوں میں" + other: "پچھلے %{count}دنوں میں" value_heading: "وَیلِیو" requirement_heading: "ضروریات" visits: "زائرین کی تعداد" @@ -2800,11 +3397,12 @@ ur: go_back: "واپس سَرچ پر" recommended: "اَپنی ضروریات کے مطابق کرنے کے لئے ہم مندرجہ ذیل ٹَیکسٹ کو اپنی مرضی کے حساب سے تبدیل کرنے کی تجویز دیں گے:" show_overriden: 'صرف اووَر رائیڈ ہوئے دکھائیں' - site_settings: + settings: show_overriden: 'صرف اووَر رائیڈ ہوئے دکھائیں' - title: 'سیٹِنگ' reset: 'رِی سَیٹ' none: 'کوئی نہیں' + site_settings: + title: 'سیٹِنگ' no_results: "کوئی نتائج نہیں پائے گئے۔" clear_filter: "صاف کریں" add_url: "URL شامل کریں" @@ -2826,6 +3424,7 @@ ur: developer: 'ڈیولپر' embedding: "اَیمبَیڈ کرنا" legal: "قانونی" + api: 'API' user_api: 'یُوزَر API' uncategorized: 'دیگر' backups: "بیک اَپس" @@ -2862,7 +3461,7 @@ ur: grant_badge: بَیج دیں granted_badges: دیے گئے بَیجز grant: عطا کریں - no_user_badges: "٪{name} کو کوئی بَیج عطا نہیں کیا گیا ہے۔" + no_user_badges: "%{name} کو کوئی بَیج عطا نہیں کیا گیا ہے۔" no_badges: عطا کیے جانے کیلئے کوئی بَیج موجود نہیں ہیں۔ none_selected: "شروع کرنے کے لئے ایک بَیج منتخب کریں" allow_title: عنوان کے طور پر بَیج وں کے استعمال کی اجازت دیں @@ -2875,6 +3474,7 @@ ur: query: بَیج قُوَیری (SQL) target_posts: ھدف پوسٹس کو قُوَیری کریں auto_revoke: روزانہ رَیوَوکَیشَن قُوَیری چلائیں + show_posts: بَیج صفحے پر بَیج دینے والی پوسٹ دکھائیں trigger: محرکات trigger_type: none: "روزانہ اَپ ڈَیٹ کریں" @@ -2895,26 +3495,27 @@ ur: no_grant_count: "کوئی بَیج تفویض کرنے کیلئے موجود نہیں۔" grant_count: one: "تفویض کرنے کیلئے 1 بَیج موجود ہے۔" - other: "تفویض کرنے کیلئے ٪{count} بَیج موجود ہیں۔" + other: "تفویض کرنے کیلئے %{count} بَیج موجود ہیں۔" sample: "نمونہ:" grant: - with: ٪{username} - with_post: ٪{username} اِس ٪{لنک} میں پوسٹ کیلئے - with_post_time: ٪{username} ٪{time} پر اِس ٪{لنک} میں پوسٹ کیلئے - with_time: ٪{username} ٪{time} پر + with: %{username} + with_post: %{username} اِس %{link} میں پوسٹ کیلئے + with_post_time: %{username} %{time} پر اِس %{link} میں پوسٹ کیلئے + with_time: %{username} %{time} پر emoji: title: "اِیمَوجی" help: "نئی اِیمَوجی شامل کریں جو سب کے لئے دستیاب ہوگی۔ (پیشہ وَرَانہ ٹِپ: ایک بار میں ایک سے زیادہ فائلوں کو ڈرَیگ & ڈراپ کریں)" add: "نئی اِیمَوجی شامل کریں" name: "نام" image: "تصویر" - delete_confirm: "کیا آپ واقعی :٪{name}: اِیمَوجی کو حذف کرنا چاہتے ہیں؟" + delete_confirm: "کیا آپ واقعی :%{name}: اِیمَوجی حذف کرنا چاہتے ہیں؟" embedding: get_started: "اگر آپ ڈِسکورس کو ایک اور ویب سائٹ پر اَیمبَیڈ کرنا چاہتے ہیں، تو ہوسٹ شامل کر کے شروع کریں۔" confirm_delete: "کیا آپ واقعی اس ہَوسٹ کو حذف کرنا چاہتے ہیں؟" sample: "ڈِسکورس ٹاپک بنانے اور اَیمبَیڈ کرنے کیلئے اپنی ویب سائٹ میں درج ذیل HTML کوڈ استعمال کریں۔ جس پیج پر آپ اسے اَیمبَیڈ کر رہے ہیں اُس کا canonical URL REPLACE_ME کی جگہ اِستعمال کریں۔" title: "اَیمبَیڈ کرنا" host: "اجازت یافتہ ہَوسٹ" + class_name: "کلاس کا نام" path_whitelist: "پاتھ وائِٹ لِسٹ" edit: "ترمیم" category: "زمرہ میں پوسٹ کریں" @@ -2934,6 +3535,7 @@ ur: embed_classname_whitelist: "اجازت یافتہ CSS کلاسوں کے نام" feed_polling_enabled: "RSS / ATOM کے ذریعے پوسٹس درآمد کریں" feed_polling_url: "کرال کرنے کیلئے RSS / ATOM کا URL" + feed_polling_frequency_mins: "فیڈ پولنگ کی فریکوئینسی (منٹوں میں)" save: "اَیمبَیڈ کرنے کی سیٹِنگ محفوظ کریں" permalink: title: "دائمی لِنکس" @@ -2955,10 +3557,13 @@ ur: done: "مکمل" back: "واپس" next: "اگلا" - step: "٪{total} میں سے ٪{current}" + step: "%{total} میں سے %{current}" upload: "اَپ لوڈ" uploading: "اَپ لوڈ کیا جا رہا ہے..." quit: "شاید بعد میں" + staff_count: + one: "آپ کی کمیونٹی میں 1 اسٹاف کا رکن ہے (آپ)۔" + other: "آپ کی کمیونٹی میں آپ سمیت، %{count} سٹاف کے ارکان ہیں۔" invites: add_user: "شامل کریں" none_added: "آپ نے کسی بھی سٹاف کو مدعو نہیں کیا۔ کیا آپ واقعی آگے جانا چاہتے ہیں؟" diff --git a/config/locales/server.ur.yml b/config/locales/server.ur.yml index ffdc57e427..ac2050ada8 100644 --- a/config/locales/server.ur.yml +++ b/config/locales/server.ur.yml @@ -6,13 +6,3352 @@ # https://www.transifex.com/projects/p/discourse-org/ ur: + dates: + short_date_no_year: "D MMM" + short_date: "D MMM, YYYY" + long_date: "MMMM D, YYYY h:mma" + datetime_formats: &datetime_formats + formats: + short: "%m-%d-%Y" + short_no_year: "%B %-d" + date_only: "%B %-d, %Y" + long: "%B %-d, %Y, %l:%M%P" + time: + am: "am" + pm: "pm" + <<: *datetime_formats title: "ڈسکورس" + topics: "ٹاپک" + posts: "پوسٹ`" + loading: "لوڈ ہو رہا ہے" + powered_by_html: 'ڈِسکورسکے مرہونِ مِنَّت، جاوااسکرپٹ کے ساتھ بہترین نظر آئے گا' + log_in: "لاگ اِن" + submit: "شائع" + purge_reason: "ترک کردہ، غیر فعال اکاؤنٹ کے طور پر خود کار طریقے سے حذف کر دیا گیا" + disable_remote_images_download_reason: "ریمَوٹ تصاویر کا ڈاؤن لوڈ غیر فعال کردیا گیا تھا کیونکہ ڈِسک میں کافی جگہ دستیاب نہیں تھی۔" + anonymous: "گمنام" + remove_posts_deleted_by_author: "مصنف کی طرف سے حذف کر دیا گیا" + redirect_warning: "ہم تصدیق نہ کر سکے کہ آپ کا منتخب کردہ لِنک اصل میں فورم پر شائع کیا گیا تھا کہ نہیں۔ اگر آپ پھر بھی آگے بڑھنا چاہتے ہیں، تو نیچے دیئے گئے لِنک کو منتخب کریں۔" + themes: + bad_color_scheme: "تِھیم اَپ ڈَیٹ نہیں ہو سکی، غلط رنگ سکیم" + other_error: "تِھیم اَپ ڈَیٹ کرتے ہوے کچھ غلط ہو گیا" + error_importing: "گِٹ رِیپَوزِٹَری کلَون کرنے میں خرابی، رسائی کی اجازت نہیں یا رِیپَوزِٹَری موجود نہیں ہے" + settings_errors: + invalid_yaml: "فراہم کردہ YAML غلط ہے۔" + data_type_not_a_number: "ترتیب `%{name}` کی قسم کی اجازت نہیں ہے۔ اجازت یافتہ اقسام `integer'،` bool`، `list` اور `enum` ہیں" + name_too_long: "بہت طویل نام کے ساتھ ایک ترتیب موجود ہے۔ زیادہ سے زیادہ لمبائی 255 ہے" + default_value_missing: "ترتیب `%{name}` کی کوئی ڈِیفالٹ قدر نہیں ہے" + default_not_match_type: "ترتیب `%{name}` کی ڈِیفالٹ قدر کی قِسم، ترتیب قِسم سے مَیچ نہیں کرتی۔" + default_out_range: "ترتیب `%{name}` کی ڈِیفالٹ قدر مخصوص رَینج میں نہیں ہے۔" + enum_value_not_valid: "منتخب کردہ قدر enum انتخاب میں سے ایک نہیں ہے۔" + number_value_not_valid: "نئی قدر اجازت یافتہ رَینج کے اندر نہیں ہے۔" + number_value_not_valid_min_max: "یہ %{min} اور %{max} کے درمیان ہونی ضروری ہے۔" + number_value_not_valid_min: "اِس کا %{min} سے زیادہ یا اِس کے برابر ہونا لازمی ہے۔" + number_value_not_valid_max: "اِس کا %{max} سے کم یا اِس کے برابر ہونا لازمی ہے۔" + string_value_not_valid: "نئی قدر کی لمبائی اجازت یافتہ رَینج کے اندر نہیں ہے۔" + string_value_not_valid_min_max: "اِس کا %{min} اور %{max} حروف لمبائی کے درمیان ہونا لازمی ہے۔" + string_value_not_valid_min: "کم از کم %{min} حروف لمبا ہونا لازمی ہے۔" + string_value_not_valid_max: "زیادہ سے زیادہ %{min} حروف لمبا ہونا لازمی ہے۔" + emails: + incoming: + default_subject: "یہ ٹاپک کو ایک عنوان کی ضرورت ہے" + show_trimmed_content: "کٹا ہوا مواد دکھائیں" + maximum_staged_user_per_email_reached: "فی اِی میل بنائے گئے سٹَیجڈ صارفین کی زیادہ سے زیادہ تعداد تک پہنچ گئے ہیں۔" + no_subject: "(کوئی موضوع نہیں)" + no_body: "(کوئی مواد نہیں)" + errors: + empty_email_error: "ہو جاتا ہے جب موصول ہونے والی راء میل خالی ہوتی ہے۔" + no_message_id_error: "ہو جاتا ہے جب میل کا 'Message-Id' ہیڈر نہ ہو۔" + auto_generated_email_error: "جب 'precedence' ہیڈر سَیٹ کیا جائے: list ،junk ،bulk یا auto_reply، پر یا جب کسی اور ہیڈر میں شامل ہو: auto-submitted ،auto-replied یا auto-generated۔" + no_body_detected_error: "ہو جاتا ہے جب ہم متن کو نکال نہ سکیں اور کوئی اٹَیچمنٹس نہ ہوں۔" + no_sender_detected_error: "ہو جاتا ہے جب ہم فرام ہیڈر میں ایک درست ای میل ایڈریس نہ تلاش کر سکیں۔" + inactive_user_error: "ہو جاتا ہے جب بھیجنے والا سرگرم نہ ہو۔" + silenced_user_error: "ہو جاتا ہے جب بھیجنے والا خاموش کیا گیا ہو۔" + bad_destination_address: "ہو جاتا ہے جب To/Cc/Bcc فیلڈز میں کوئی بھی ای میل ایڈریس ترتیب کردہ آنے والے ای میل ایڈریسوں سے مَیچ نہ کرے۔" + strangers_not_allowed_error: "ہو جاتا ہے جب کسی صارف نے ایک ایسے زُمرہ میں نیا ٹاپک بنانے کی کوشش کی ہو جس کا وہ رکن نہ ہوں۔" + insufficient_trust_level_error: "ہو جاتا ہے جب کسی صارف نے ایک ایسے زُمرہ میں نیا ٹاپک بنانے کی کوشش کی ہو جس پر اُن کے پاس مطلوبہ ٹرسٹ لَیول نہ ہو۔" + reply_user_not_matching_error: "ہو جاتا ہے جب جواب ایک ایسے ای میل ایڈریس سے آیا ہو جو اطلاع بھیجے جانے والے ایڈریس سے مختلف ہو۔" + topic_not_found_error: "ہو جاتا ہے جب جواب آجائے لیکن متعلقہ ٹاپک حذف کر دیا گیا ہو۔" + topic_closed_error: "ہو جاتا ہے جب جواب آجائے لیکن متعلقہ ٹاپک بند کر دیا گیا ہو۔" + bounced_email_error: "یہ ای میل ایک لوٹا دیئے جانے والی ای میل کی رپورٹ ہے۔" + screened_email_error: "ہو جاتا ہے جب بھیجنے والے کا ای میل ایڈریس پہلے سے ہی سکرین ہوا ہو۔" + unsubscribe_not_allowed: "ہو جاتا ہے جب اِس صارف کیلئے ای میل کے ذریعے سبسکرِپشن ختم کرنے کی اجازت نہ ہو۔" + email_not_allowed: "جب ای میل ایڈریس وائٹ لِسٹ میں نہ ہو یا بلیک لِسٹ پر ہو۔" + unrecognized_error: "نامعلوم خرابی" errors: &errors + format: '%{attribute} %{message}' + format_with_full_message: '%{attribute}: %{message}' messages: - empty: ھالی نہیں ھو سکتا + too_long_validation: "%{max} حروف تک محدود ہے؛ آپ نے %{length} درج کیے۔" + invalid_boolean: "غلط بُولِیَّن۔" + taken: "پہلے سے ہی لیا جا چکا ہے" + accepted: منظور کرنا لازمی ہے + blank: خالی نہیں ہو سکتا + present: خالی ہونا لازمی ہے + confirmation: "%{attribute}کے ساتھ مماثلت نہیں رکھتا " + empty: خالی نہیں ہو سکتا + equal_to: '%{count} کے برابر ہونا لازمی ہے' + even: غیر طاق ہونا لازمی ہے + exclusion: مخصوص ہے + greater_than: '%{count}سے زیادہ ہونا لازمی ہے ' + greater_than_or_equal_to: '%{count}سے زیادہ یا اُس کے برابر ہونا لازمی ہے ' + has_already_been_used: "پہلے سے ہی استعمال کیا جا چکا ہے" + inclusion: فہرست میں شامل نہیں ہے + invalid: غلط ہے + is_invalid: "واضح نہیں لگتا، کیا یہ ایک مکمل جملہ ہے؟" + contains_censored_words: "درج ذیل سَینسَر کیے گئے الفاظ پر مشتمل ہے: %{censored_words}" + less_than: '%{count}سے کم ہونا لازمی ہے ' + less_than_or_equal_to: '%{count}سے کم یا اُس کے برابر ہونا لازمی ہے ' + not_a_number: نمبر نہیں ہے + not_an_integer: اِنٹیجَر ہونا لازمی ہے + odd: طاق ہونا لازمی ہے + record_invalid: 'توثیق ناکام ہو گئی: %{errors}' + max_emojis: "%{max_emojis_count} سے زیادہ اِیمَوجی نہیں ہو سکتیں" + ip_address_already_screened: "پہلے سے موجودہ ایک اصول میں شامل ہے" + restrict_dependent_destroy: + one: "ریکارڈ حذف نہیں کیا جا سکتا کیونکہ ایک انحصار %{record} موجود ہے" + many: "ریکارڈ حذف نہیں کیا جا سکتا کیونکہ ایک انحصار %{record} موجود ہے" + too_long: + one: بہت لمبا ہے (زیادہ سے زیادہ 1 حرف ہو سکتا ہے) + other: بہت لمبا ہے (زیادہ سے زیادہ %{count} حروف ہو سکتے ہیں) + too_short: + one: بہت مختصر ہے (کم از کم 1 حرف ہو سکتا ہے) + other: بہت مختصر ہے (کم از کم %{count} حروف ہو سکتے ہیں) + wrong_length: + one: غلط لمبائی ہے (1 حرف لمبا ہونا چاہئے) + other: غلط لمبائی ہے (%{count} حروف لمبا ہونا چاہئے) + other_than: "%{count}کے علاوہ ہونا لازمی ہے " + template: + body: 'درج ذیل خانوں کے ساتھ مسائل تھے:' + header: + one: '1 خرابی نے اِس %{model} کو محفوظ ہونے سے روک دیا' + other: '%{count}خرابیوں نے اِس %{model} کو محفوظ ہونے سے روک دیا' + embed: + load_from_remote: "اُس پوسٹ کو لوڈ کرنے میں ایک خرابی کا سامنا کرنا پڑا۔" + site_settings: + min_username_length_exists: "آپ صارف نام کی کم از کم لمبائی، سب سے مختصر صارف نام کی لمبائی سے کم مقرر نہیں کرسکتے۔" + min_username_length_range: "آپ کم از کم، زیادہ سے زیادہ سے اوپر مقرر نہیں کرسکتے۔" + max_username_length_exists: "آپ صارف نام کی زیادہ سے زیادہ لمبائی، سب سے طویل صارف نام کی لمبائی سے کم مقرر نہیں کرسکتے۔" + max_username_length_range: "آپ زیادہ سے زیادہ، کم از کم سے نیچے مقرر نہیں کرسکتے۔" + default_categories_already_selected: "آپ کسی دوسری فہرست میں استعمال کردہ زُمرَہ کا انتخاب نہیں کرسکتے۔" + s3_upload_bucket_is_required: "آپ S3 پر اَپ لوڈز فعال نہیں کرسکتے جب تک کہ آپ نے 's3_upload_bucket' فراہم نہیں کیا۔" + conflicting_google_user_id: 'اِس اکاؤنٹ کے لئے گُوگَل اکاؤنٹ ID تبدیل ہوگئی ہے؛ سیکیورٹی وجوہات کی بناہ پر اسٹاف کی مداخلت کی ضرورت ہے۔ براہ مہربانی اسٹاف سے رابطہ کریں اور اُنہیں اشارہ کریں
    https://meta.discourse.org/t/76575' + invite: + not_found: "آپ کا دعوتی ٹوکن غلط ہے۔ براہ کرم سائٹ کے ایڈمِنِسٹریٹر سے رابطہ کریں۔" + user_exists: "%{email}کو مدعو کرنے کی کوئی ضرورت نہیں ہے، اُن کا پہلے سے ہی ایک اکاؤنٹ ہے!" + bulk_invite: + file_should_be_csv: "اَپ لوڈ شدہ فائل CSV فارمیٹ میں ہونی چاہئے۔" + error: "یہ فائل اَپ لوڈ کرنے میں ایک خرابی کا سامنا کرنا پڑا۔ براہ مہربانی کچھ دیر بعد دوبارہ کوشش کریں۔" + topic_invite: + user_exists: "معذرت، وہ صارف پہلے ہی مدعو کیا جا چکا ہے۔ آپ صارف کو ایک ٹاپک پر صرف ایک ہی دفعہ مدعو کرسکتے ہیں۔" + backup: + operation_already_running: "ایک کارروائی اِس وقت چل رہی ہے۔ ابھی ایک نیا کام شروع نہیں کیا جا سکتا۔" + backup_file_should_be_tar_gz: "بیک اَپ فائل کو .tar.gz آرکائیو ہونا چاہئے۔" + not_enough_space_on_disk: "اس بیک اَپ کو اَپ لوڈ کرنے کیلئے ڈِسک میں کافی جگہ نہیں ہے۔" + invalid_filename: " بیک اَپ فائل کے نام میں غلط حروف شامل ہیں۔ درست حروف a-z 0-9 . - _ ہیں۔" + invalid_params: "آپ نے درخواست پر غلط پیرامیٹرز فراہم کیے ہیں: %{message}" + not_logged_in: "آپ کو یہ کرنے کیلئے لاگ اِن ہونا ضروری ہے۔" + not_found: "مطلوبہ URL یا ریسورس نہیں مل سکی۔" + invalid_access: "آپ کو درخواست کردہ ریسورس کو دیکھنے کی اجازت نہیں ہے۔" + invalid_api_credentials: "آپ کو درخواست کردہ ریسورس کو دیکھنے کی اجازت نہیں ہے۔ API کا صارف نام یا کِی غلط ہے۔" + read_only_mode_enabled: "یہ سائٹ صرف پڑھنے کے مَوڈ میں ہے۔ اِنٹرَیکشنز غیر فعال ہیں۔" + reading_time: "پڑھنے کیلئے وقت" + likes: "لائیکس" + too_many_replies: + one: "ہم معذرت خواہ ہیں، لیکن نئے صارفین کو عارضی طور پر ایک ٹاپک میں 1 جواب تک محدود کیا گیا ہے۔" + other: "ہم معذرت خواہ ہیں، لیکن نئے صارفین کو عارضی طور پر ایک ٹاپک میں %{count} جوابات تک محدود کیا گیا ہے۔" + embed: + start_discussion: "بحث شروع کریں" + continue: "بحث جاری رکھیں" + error: "اَیمبَیڈ کرنے میں خرابی" + referer: "حوالہ دہندہ:" + mismatch: "حوالہ دہندہ مندرجہ ذیل ہوسٹس میں سے کسی کے ساتھ بھی مَیچ نہیں کیا:" + no_hosts: "اَیمبَیڈ کرنے کیلئے کوئی ہوسٹس سَیٹ نہیں کیے گئے۔" + configure: "اَیمبَیڈ کرنا ترتیب دیں" + more_replies: + one: "1 مزید جواب" + other: "%{count}مزید جوابات " + loading: "بحث لَوڈ ہو رہی ہے..." + permalink: "دائمی لِنک" + imported_from: "%{link}پر اصل اندراج کیلئے یہ ایک ساتھی بحث ٹاپک ہے " + in_reply_to: "◀ %{username}" + replies: + one: "1 جواب" + other: "%{count}جوابات " + no_mentions_allowed: "معذرت، آپ دوسرے صارفین کا ذکر نہیں کر سکتے۔" + too_many_mentions: + one: "معذرت، آپ ایک پوسٹ میں صرف ایک صارف کا ذکر کر سکتے ہیں۔" + other: "معذرت، آپ ایک پوسٹ میں صرف %{count} صارفین کا ذکر کر سکتے ہیں۔" + no_mentions_allowed_newuser: "معذرت، نئے صارفین دوسرے صارفین کا ذکر نہیں کر سکتے۔" + too_many_mentions_newuser: + one: "معذرت، نئے صارفین ایک پوسٹ میں صرف ایک دوسرے صارف کا ذکر کر سکتے ہیں۔" + other: "معذرت، نئے صارفین ایک پوسٹ میں صرف %{count} صارفین کا ذکر کر سکتے ہیں۔" + no_images_allowed_trust: "معذرت، آپ پوسٹ میں تصاویر نہیں ڈال سکتے" + no_images_allowed: "معذرت، نئے صارفین پوسٹس میں تصاویر نہیں ڈال سکتے۔" + too_many_images: + one: "معذرت، نئے صارفین ایک پوسٹ میں صرف ایک تصویر ڈال سکتے ہیں۔" + other: "معذرت، نئے صارفین ایک پوسٹ میں صرف %{count} تصاویر ڈال سکتے ہیں۔" + no_attachments_allowed: "معذرت، نئے صارفین پوسٹس میں اَٹَیچمنٹس نہیں ڈال سکتے۔" + too_many_attachments: + one: "معذرت، نئے صارفین ایک پوسٹ میں صرف ایک اَٹَیچمنٹ ڈال سکتے ہیں۔" + other: "معذرت، نئے صارفین ایک پوسٹ میں صرف %{count} اَٹَیچمنٹس ڈال سکتے ہیں۔" + no_links_allowed: "معذرت، نئے صارفین پوسٹس میں لِنکس نہیں ڈال سکتے۔" + links_require_trust: "معذرت، آپ اپنی پوسٹس میں لِنکس شامل نہیں کر سکتے۔" + too_many_links: + one: "معذرت، نئے صارفین ایک پوسٹ میں صرف ایک لِنک ڈال سکتے ہیں۔" + other: "معذرت، نئے صارفین ایک پوسٹ میں صرف %{count} لِنکس ڈال سکتے ہیں۔" + contains_blocked_words: "آپ کی پوسٹ میں ایک ایسا لفظ شامل ہے جس کی اجازت نہیں ہے: %{word}" + spamming_host: "معذرت، آپ اس ہَوسٹ کیلئے لِنک پوسٹ نہیں کر سکتے۔" + user_is_suspended: "معطل صارفین کو پوسٹ کرنے کی اجازت نہیں ہے۔" + topic_not_found: "کچھ غلط ہو گیا ہے۔ شاید جس دوران آپ اِسے دیکھ رہے تھے، یہ ٹاپک بند یا حذف کر دیا گیا؟" + not_accepting_pms: "معذرت، %{username} اِس وقت پیغامات قبول نہیں کر رہا ہے۔" + max_pm_recepients: "معذرت، آپ زیادہ سے زیادہ %{recipients_limit} وصول کنندگان کو ایک پیغام بھیج سکتے ہیں۔" + just_posted_that: "آپ نے جو حال ہی میں پوسٹ کیا ہے، یہ اُسی کی طرح کا بہت زیادہ ہے" + invalid_characters: "غلط حروف شامل ہیں" + is_invalid: "واضح نہیں لگتا، کیا یہ ایک مکمل جملہ ہے؟" + next_page: "اگلا صفحہ ←" + prev_page: "→ پچھلا صفح" + page_num: "صفحہ نمبر %{num}" + home_title: "ہَوم" + topics_in_category: "'%{category}' زُمرہ میں ٹاپک" + rss_posts_in_topic: "'%{topic}' کی RSS فِیڈ" + rss_topics_in_category: "'%{category}' زُمرہ میں ٹاپکس کی RSS فِیڈ" + author_wrote: "%{author} نے لکھا: " + num_posts: "پوسٹس:" + num_participants: "شرکاء:" + read_full_topic: "مکمل ٹاپک پڑھیں" + private_message_abbrev: "پغم" + rss_description: + latest: "تازہ ترین ٹاپک" + hot: "گرم ٹاپک" + top: "ٹاپ ٹاپک" + top_all: "تمام وقت کے ٹاپ ٹاپک" + top_yearly: "سالانہ ٹاپ ٹاپک" + top_quarterly: "سہ ماہی ٹاپ ٹاپک" + top_monthly: "ماہانہ ٹاپ ٹاپک" + top_weekly: "ہفتہ وار ٹاپ ٹاپک" + top_daily: "روزانہ کے ٹاپ ٹاپک" + posts: "تازہ ترین پوسٹس" + private_posts: "تازہ ترین ذاتی پیغامات" + group_posts: "%{group_name} کی طرف سے تازہ ترین پوسٹس " + group_mentions: "%{group_name} کی طرف سے تازہ ترین ذکر" + user_posts: "@%{username} کی طرف سے تازہ ترین پوسٹس" + user_topics: "@%{username} کی طرف سے تازہ ترین ٹاپک" + tag: "ٹیگ ہوے ٹاپک" + badge: "%{display_name} پر %{site_title} بَیج" + too_late_to_edit: "یہ پوسٹ بہت دیر پہلے بنائی گئی تھی۔ یہ مزید ترمیم یا حذف نہیں کی جا سکتی۔" + revert_version_same: "موجودہ وَرژن وہی وَرژن ہے جسے آپ واپس لوٹانے کی کوشش کررہے ہیں۔" + excerpt_image: "تصویر" + queue: + delete_reason: "پوسٹ ماڈرَیشَن قطار کے ذریعے حذف کر دیا گیا" + not_found: "پوسٹ نہیں مل سکی یا پہلے سے ہی اَپ ڈیٹ ہو چکی ہے۔" + groups: + success: + bulk_add: "گروپ میں %{users_added} صارفین کو شامل کر لیا گیا ہے۔" + errors: + can_not_modify_automatic: "آپ ایک خودکار گروپ کو ترمیم نہیں کر سکتے ہیں" + invalid_domain: "'%{domain}' ایک درست ڈَومَین نہیں ہے۔" + invalid_incoming_email: "'%{domain}' ایک درست ایِ میل ایڈریس نہیں ہے۔" + email_already_used_in_group: "'%{email}' پہلے ہی گروپ '%{group_name}' کے ذیرِاستعمال ہے۔" + email_already_used_in_category: "'%{email}' پہلے ہی زُمرہ '%{category_name}' کے ذیرِاستعمال ہے۔" + cant_allow_membership_requests: "آپ ایک ایسا گروپ جس کا کوئی مالک نہ ہو، اُس کیلئے رکنیت کی درخواستوں کی اجازت نہیں دے سکتے۔" + default_names: + everyone: "ہر کوئی" + admins: "ایڈمن" + moderators: "ماڈریٹرز" + staff: "اسٹاف" + trust_level_0: "ٹرسٹ_لَیول_0" + trust_level_1: "ٹرسٹ_لَیول_1" + trust_level_2: "ٹرسٹ_لَیول_2" + trust_level_3: "ٹرسٹ_لَیول_3" + trust_level_4: "ٹرسٹ_لَیول_4" + request_membership_pm: + title: "@%{group_name} کیلئے رکنیت کی درخواست" + education: + until_posts: + one: "1 پوسٹ" + other: "%{count} پوسٹس" + new-topic: | + %{site_name}پر خوش آمدید &mdash؛ **نئی بات چیت شروع کرنے کیلئے شکریہ!** + + - اگر آپ عنوان کو زبان سے پڑھیں تو کیا یہ دِلچسپ سنائی دیتا ہے؟ کیا یہ ایک اچھا خلاصہ ہے؟ + + - کون اِس میں دلچسپی رکھے گا؟ یہ کیوں ضروری ہے؟ آپ کو کس قِسم کے جوابات چاہیئیں؟ + + - امنے ٹاپک میں عام طور پر استعمال کردہ الفاظ شامل کریں تاکہ دوسرے اِسے *تلاش* کرسکیں۔ اپنے ٹاپک کو متعلقہ ٹاپکس کے ساتھ گروہ کرنے کیلئے، ایک زُمرہ منتخب کریں۔ + + مزید کے لئے، [ہماری کمیونٹی کی رہنما ہدایات دیکھیے](/guidelines)۔ یہ پَینل صرف آپ کے پہلے %{education_posts_text} کیلئے ظاہر ہوگا۔ + new-reply: | + %{site_name}پر خوش آمدید &mdash؛ **شراکت کیلئے شکریہ!** + + - کیا آپ کا جواب کسی طرح سے اِس بات چیت کو بہتر بناتا ہے؟ + + - اپنے کمیونٹی کے ارکان کے ساتھ مہربان ہوں۔ + + - تعمیراتی تنقید قابل قبول ہے، لیکن تنقید *خیالات* پر کریں، نہ کہ لوگوں پر۔ + + مزید کے لئے، [ہماری کمیونٹی کی رہنما ہدایات دیکھیے](/guidelines)۔ یہ پَینل صرف آپ کے پہلے %{education_posts_text} کیلئے ظاہر ہوگا۔ + avatar: | + ### آپ کے اکاؤنٹ کیلئے ایک تصویر کیسی لگے گی؟ + + آپ نے چند ٹاپک اور جوابات پوسٹ کیے ہیں، لیکن آپ کی پروفائل تصویر آپ کی طرح منفرد نہیں ہے – یہ صرف ایک حرف ہے۔ + + کیا آپ نے اِس بات پر غور کیا ہے کہ آپ **[اپنے صارف پروفائل پر جا کر](%{profile_path})** اور ایسی تصویر اَپ لَوڈ کریں جو آپ کی نمائندگی کرتی ہو؟ + + بات چیت کی پیروی کرنا اور دلچسپ لوگوں کو بات چیت میں تلاش کرنا آسان ہو جاتا ہے جب سب کی ایک منفرد پروفائل تصویر ہے! + sequential_replies: | + ### ایک ساتھ کئی پوسٹس کا جواب دینے پر غور کریں + + ٹاپک پر ایک سلسلہ میں کئی جوابوں کے بجائے، براہ کرم ایک ہی جواب، جس میں پچھلی پوسٹس کے اقتباس یا @نام کے حوالے شامل ہوں، پر غور کریں۔ + + متن کو اُجاگر کر کہ ظاہر ہونے والے جواب اقتباس کریں بٹن منتخب کرنے سے ایک اقتباس شامل کرنے کیلئے آپ اپنے پچھلے جواب میں ترمیم کرسکتے ہیں۔ + + ہر ایک کیلئے اُن ٹاپکس کو پڑھ نا زیادہ آسان ہے جس میں بہت سے چھوٹے، انفرادی جوابات کے مقابلے میں تفصیلی لیکن کم جوابات ہوں۔ + dominating_topic: | + ### دوسروں کو گفتگو میں شامل ہونے کی اجازت دیں + + یہ ٹاپک آپ کے لئے واضح طور پر اہم ہے– آپ نے یہاں %{percent}% سے زیادہ جوابات پوسٹ کیے ہیں۔ + + کیا آپ کو یقین ہے کہ آپ دوسروں کو اپنے نقطہ نظر کا اشتراک کرنے کیلئے کافی وقت فراہم کررہے ہیں؟ + get_a_room: | + ### مزید لوگوں کو جواب دینے پر غور کریں + + آپ نے اِس مخصوص ٹاپک میں پہلے ہی @%{reply_username} کو %{count} مرتبہ جواب دیا ہے۔ + + کیا آپ نے بحث میں *دیگر* لوگوں کو بھی جواب دیبے کا سوچا ہے؟ ایک عظیم بحث میں مختلف آرا اور نقطہ نظر شامل ہوتے ہیں۔ + + اگر آپ اِس مخصوص صارف کے ساتھ اپنی گفتگو لمبائی میں جاری رکھنا چاہتے ہیں، تو [اِنہیں ایک ذاتی پیغام بھیجیں](/u/%{reply_username})۔ + too_many_replies: | + ### آپ اس ٹاپک پر جوابات کے نمبر کی حد تک پہنچ گئے ہیں + + ہم معذرت خواہ ہیں، لیکن نئے صارفین کو عارضی طور پر ایک ٹاپک میں %{newuser_max_replies_per_topic} جوابات تک محدود کیا گیا ہے۔ + + ایک اور جواب کو شامل کرنے کے بجائے، براہ کرم اپنے پچھلے جوابوں میں ترمیم، یا دیگر ٹاپکس ملاحظہ کرنے کے بارے میں سوچیں۔ + reviving_old_topic: | + ### اِس ٹاپک کو بحال کریں؟ + + اِس ٹاپک پر آخری جواب **** تھا۔ آپ کا جواب ٹاپک کو اپنی فہرست کے سب سے اوپر پہنچا دے گا اور گفتگو میں پہلے ملوث تمام افراد کو مطلع کردے گا۔ + + کیا آپ واقعی یہ پرانی گفتگو جاری رکھنا چاہتے ہیں؟ + activerecord: + attributes: + category: + name: "زُمرہ کا نام" + topic: + title: 'عنوان' + featured_link: 'نمایاں لنک' + post: + raw: "متن" + user_profile: + bio_raw: "میرے بارے میں" + errors: + models: + topic: + attributes: + base: + warning_requires_pm: "آپ ذاتی پیغامات کے ساتھ صرف انتباہات منسلک کرسکتے ہیں۔" + too_many_users: "آپ انتباہات ایک وقت میں صرف ایک صارف کو بھیج سکتے ہیں۔" + cant_send_pm: "معذرت، آپ اُس صارف کو ذاتی پیغام نہیں بھیج سکتے۔" + no_user_selected: "آپ کا ایک درست صارف منتخب کرنا ضروری ہے۔" + reply_by_email_disabled: "ای میل کے ذریعے جواب دینا غیر فعال کردیا گیا ہے۔" + target_user_not_found: "جن صارفین کو آپ یہ پیغام بھیج رہے ہیں اُن میں سے ایک نہ مل سکا۔" + featured_link: + invalid: "غلط ہے۔ URL میں http:// یا https:// شامل ہونا چاہئے۔" + invalid_category: "اِس زُمرہ میں ترمیم نہیں کیا جا سکتا۔" + user: + attributes: + password: + common: "سب سے زیادہ عام 10000 پاسورڈز میں سے ایک ہے۔ براہ مہربانی مزید محفوظ پاسورڈ کا استعمال کریں۔" + same_as_username: "آپ کے صارف نام جیسا ہی ہے۔ براہ مہربانی مزید محفوظ پاسورڈ کا استعمال کریں۔" + same_as_email: "آپ کے ای میل جیسا ہی ہے۔ براہ مہربانی مزید محفوظ پاسورڈ کا استعمال کریں۔" + same_as_current: "آپ کے موجودہ پاسورڈ جیسا ہی ہے۔" + unique_characters: "بار بار اِستعمال ہونے والے بہت زیادہ حروف شامل ہیں۔ براہ مہربانی مزید محفوظ پاسورڈ کا استعمال کریں۔" + ip_address: + signup_not_allowed: "اِس اکاؤنٹ سے سائن اَپ کی اجازت نہیں ہے۔" + user_email: + attributes: + user_id: + reassigning_primary_email: "کسی دوسرے صارف کو ایک بنیادی ای میل دوبارہ تفویض کرنے کی اجازت نہیں ہے۔" + color_scheme_color: + attributes: + hex: + invalid: "درست رنگ نہیں ہے" + post_reply: + base: + different_topic: "پوسٹ اور جواب ایک ہی ٹاپک سے منسلک ہونے چاہئیں۔" + web_hook: + attributes: + payload_url: + invalid: "URL غلط ہے۔ URL میں http:// یا https:// شامل ہونا چاہئے۔ اور کوئی خالی جگہ کی اجازت نہیں ہے۔" + custom_emoji: + attributes: + name: + taken: پہلے سے ہی ایک دوسرے اِیمَوجی کے ذیرِ استعمال ہے + topic_timer: + attributes: + execute_at: + in_the_past: "مستقبل میں ہونا ضروری ہے۔" + translation_overrides: + attributes: + value: + invalid_interpolation_keys: 'مندرجہ ذیل اِنٹَرپَولَیشَن کیز غلط ہیں: "%{keys}"' + watched_word: + attributes: + word: + too_many: "اس کارروائی کیلئے بہت زیادہ الفاظ" + <<: *errors + user_profile: + no_info_me: "
    آپ کی پروفائل کا \"میرے بارے میں\" والی فیلڈ فی الحال خالی ہے، کیا آپ اسے بھرنا پسند کریں گے؟
    " + no_info_other: "
    %{name}نے اپنی پروفائل کے \"میرے بارے میں\" والی فیلڈ میں ابھی تک کچھ نہیں درج کیا
    " + vip_category_name: "لاؤنج" + vip_category_description: "ٹرسٹ لَیول 3 یا اُس سے زیادہ والے ارکان کیلئے خاص زُمرہ۔" + meta_category_name: "سائٹ کیلئے رائے" + meta_category_description: "اِس سائٹ کے بارے میں بحث، اِس کی تنظیم، یہ کیسے کام کرتی ہے، اور ہم کس طرح اِسے بہتر بنا سکتے ہیں۔" + staff_category_name: "سٹاف" + staff_category_description: "اسٹاف کی گفتگو کیلئے ذاتی زُمرہ۔ ٹاپک صرف ایڈمن اور ماڈریٹرز کو نظر آتے ہیں۔" + assets_topic_title: "سائٹ کے ڈیزائن کیلئے اثاثہ جات" + assets_topic_body: "یہ ٹاپک، جو صرف اسٹاف کو نظر آتا ہے، سائٹ کے ڈیزائن میں استعمال ہونے والی تصاویر اور فائلوں کو رکھنے کیلئے ہے۔ اسے حذف نہ کیجیے!\n\n\nیہ اِس طرح سے ہے:\n\n\n1. اس ٹاپک پر جواب دیں۔\n2. تمام تصاویر جنہیں آپ لوگو، فیوکان یا دوسری چیزوں کیلئے استعمال کرنا چاہتے ہیں یہاں اَپ لوڈ کریں۔ (پوسٹ ایڈیٹر میں اپلوڈ ٹول بار آئیکن کا استعمال کریں، یا ڈریگ اور ڈراپ یا تصاویر پیسٹ کریں۔)\n3. پوسٹ کرنے کیلئے اپنا جواب شائع کریں۔\n4. اپ لوڈ کردہ تصاویر کے پاتھ حاصل کرنے کیلئے اپنی نئی پوسٹ میں تصاویر پر دائیں کلِک کریں، یا اپنی پوسٹ میں ترمیم کرنے کیلئے ترمیم آئیکن پر کلِک کریں اور تصاویر کے پاتھ حاصل کریں۔ تصویر کا پاتھ کاپی کریں۔\n5. اِن تصاویر کے پاتھ کو [بنیادی ترتیبات](/admin/site_settings/category/required) میں پَیسٹ کریں۔\n\n\nاگر آپ کو مختلف فائل اِقسام کے اَپ لوڈ کو فعال کرنے کی ضرورت ہے تو، [فائل ترتیبات]() کت اندر `authorized_extensions` میں ترمیم کریں۔" + discourse_welcome_topic: + title: "ڈسکورس میں خوش آمدید" + body: |2 + + اس پِن کیے گئے ٹاپک کا پہلا پیراگراف آپ کے ہَوم پیج پر تمام نئے زائرین کو ایک خوش آمدید پیغام کے طور پر نظر آئے گا۔ یہ اہم ہے! + + **اس میں ترمیم کریں** آپنی کمیونٹی کی ایک مختصر وضاحت میں: + + یہ کس کیلئے ہے؟ + وہ یہاں کیا تلاش کرسکتے ہیں؟ + وہ یہاں کیوں آئیں؟ + - وہ مزید کہاں پڑھ سکتے ہیں (لنکس، وسائل، وغیرہ)؟ + + + + آپ اَیڈمن :wrench: (اوپری دائیں اور سب سے نیچے) کے ذریعہ اِس ٹاپک کو بند کرنا چاہیں گے، تاکہ ایک اعلان پر جوابات کا ڈھیر نہ لگ جائے۔ + lounge_welcome: + title: "لاؤنج میں خوش آمدید" + body: |2 + + مبارک باد! :confetti_ball: + + اگر آپ اِس ٹاپک کو دیکھ سکتے ہیں، تو آپ کو حال ہی میں **رَیگولر** (ٹرسٹ لَیول 3) پر ترقی دے دی گئی ہے۔ + + اب آپ … + + * کسی بھی ٹاپک کے عنوان میں ترمیم کر سکتے ہیں + * کسی بھی ٹاپک کا زُمرہ تبدیل کر سکتے ہیں + * اپنے تمام لِنکس فَولَو کروا سکتے ہیں ([خود کار طریقے سےغیر فَولَو](http://en.wikipedia.org/wiki/Nofollow) is removed) ہٹا دیا جاتا ہے) + * ایک ذاتی لاؤنج زُمرہ جو صرف ٹرسٹ لَیول 3 یا اُس سے زیادہ والے صارفین کو نظر آتا ہے تک رسائی کر سکتے ہیں + * ایک ہی فلَیگ سے سپَیم چھپا سکتے ہیں + + یہاں [ساتھی رَیگولرز کی موجودہ فہرست](/badges/3/regular) ہے۔ ہیلو کہنا نہ بھولیے گا۔ + + اِس کمیونٹی کا ایک اہم حصہ ہونے کا شکریہ! + + (ٹرسٹ لَیول پر مزید معلومات کیلئے، [یہ ٹاپک دیکھیے][اعتماد]۔ براہ کرم نوٹ کریں کہ صرف ایسے ممبران جو وقت کے ساتھ شرائط کو پورا کرتے رہیں گے صرف وہی رَیگولرز میں شامل رہیں گے۔) + + [اعتماد]: https://meta.discourse.org/t/what-do-user-trust-levels-do/4924 + category: + topic_prefix: "%{category} زُمرہ کے بارے میں" + replace_paragraph: "(اس پہلے پیراگراف کو اپنے نئے زُمرہ کے بارے میں ایک مختصر وضاحت کے ساتھ تبدیل کریں۔ یہ ہدایات زُمرہ کے انتخاب کے والی جگہ پر نظر آئے گا، لہٰذا اِسے 200 حروف سے نیچے رکھنے کی کوشش کریں۔ **جب تک آپ اِس وضاحت میں ترمیم یا ٹاپک نہیں بنائیں گے، یہ زُمرہ زُمرہ جات کے صفحہ پر نہیں دکھایا جائے گا۔**)" + post_template: "%{replace_paragraph}\n\nطویل تر وضاحت، یا زُمرہ کے قواعد و ضوابط قائم کرنے کیلئے مندرجہ ذیل پیراگراف کا استعمال کریں:\n\n- لوگوں کو اِس زُمرہ کو کیوں استعمال کرنا چاہئے؟ یہ کس لیے ہے؟ \n\n- ہمارے پاس پہلے ہی سے موجود دیگر زُمرہ جات کے مقابلے میں یہ کس طرح مختلف ہے؟\n\n- اس زُمرہ کے ٹاپکس میں عام طور پر کیا ہونا چاہئے؟\n\nکیا ہمیں اِس زُمرہ کی ضرورت ہے؟ کیا ہم کسی اور زُمرہ، یا ذیلی زمرہ کے ساتھ ضم کر سکتے ہیں؟\n" + errors: + not_found: "زُمرہ نہیں ملا!" + uncategorized_parent: "بِلا زُمرہ والوں کا بالائی زُمرہ نہیں ہو سکتا" + self_parent: "ایک ذیلی زُمرہ کا بالائی وہ خود ہی نہیں ہوسکتا" + depth: "آپ ایک ذیلی زُمرہ کو کسی دوسرے کے تحت نہیں کر سکتے" + invalid_email_in: "'%{email}' ایک درست ای میل اَیڈرَیس نہیں ہے۔" + email_already_used_in_group: "'%{email}' پہلے ہی گروپ '%{group_name}' کے ذیرِاستعمال ہے۔" + email_already_used_in_category: "'%{email}' پہلے ہی زُمرہ '%{category_name}' کے ذیرِاستعمال ہے۔" + description_incomplete: "زُمرہ کی وضاحت والی پوسٹ کا کم ازکم ایک پیراگراف ہونا ضروری ہے۔" + cannot_delete: + uncategorized: "بِلا زُمرہ والوں کو حذف نہیں کیا جا سکتا" + has_subcategories: "اِس زُمرہ کو حذف نہیں کیا جا سکتا کیونکہ اِس کے ذیلی زُمرہ ہیں۔" + topic_exists: + one: "اِس زُمرہ کو حذف نہیں کیا جا سکتا کیونکہ اِس میں 1 ٹاپک ہے۔ سب سے پرانا ٹاپک %{topic_link} ہے۔" + other: "اِس زُمرہ کو حذف نہیں کیا جا سکتا کیونکہ اِس میں %{count} ٹاپک ہیں۔ سب سے پرانا ٹاپک %{topic_link} ہے۔" + topic_exists_no_oldest: "اِس زُمرہ کو حذف نہیں کیا جا سکتا کیونکہ ٹاپک تعداد %{count} ہے۔" + uncategorized_description: "وہ ٹاپک جنہیں کسی زُمرہ کی ضرورت نہیں، یا جو کسی اور موجودہ زُمرہ میں فِٹ نہیں ہوتے۔" + trust_levels: + newuser: + title: "نیا صارف" + basic: + title: "بَیسِک صارف" + member: + title: "ممبر" + regular: + title: "رَیگولر" + leader: + title: "لِیڈر" + change_failed_explanation: "آپ نے %{user_name} کو '%{new_trust_level}' تک درجہ کم کرنے کی کوشش کی۔ تاہم اُن کا ٹرسٹ لَیول پہلے سے ہی '%{current_trust_level}' ہے۔ %{user_name} '%{current_trust_level}' پر رہیں گے - اگر آپ صارف کا درجہ کم کرنا چاہتے ہیں تو پہلے ٹرسٹ لَیول لاک کیجیے" + post: + image_placeholder: + broken: "یہ تصویر ٹوٹی ہوئی ہے" + rate_limiter: + slow_down: "آپ یہ کارروائی بہت بار کر چکے ہیں، بعد میں دوبارہ کوشش کیجیے۔" + too_many_requests: "آپ نے یہ کارروائی بہت زیادہ دفعہ کی ہے۔ براہ کرم دوبارہ کوشش کرنے سے قبل %{time_left} انتظار کریں۔" + by_type: + first_day_replies_per_day: "آپ نئے صارف کیلئے اپنے پہلے دن پر زیادہ سے زیادہ جوابات کی حد تک پہنچ گئے ہیں۔ براہ کرم دوبارہ کوشش کرنے سے قبل %{time_left} انتظار کریں۔" + first_day_topics_per_day: "آپ نئے صارف کیلئے اپنے پہلے دن پر زیادہ سے زیادہ بنائے جانے والے ٹاپکس کی حد تک پہنچ گئے ہیں۔ براہ کرم دوبارہ کوشش کرنے سے قبل %{time_left} انتظار کریں۔" + create_topic: "آپ بہت جلدی سے ٹاپکس بنائے جا رہے ہیں۔ براہ کرم دوبارہ کوشش کرنے سے قبل %{time_left} انتظار کریں۔" + create_post: "آپ بہت جلدی سے جواب دیے جا رہے ہیں۔ براہ کرم دوبارہ کوشش کرنے سے قبل %{time_left} انتظار کریں۔" + delete_post: "آپ بہت جلدی سے پوسٹس حذف کرتے جا رہے ہیں۔ براہ کرم دوبارہ کوشش کرنے سے قبل %{time_left} انتظار کریں۔" + public_group_membership: "آپ بہت کثرت سے گروپوں میں شامل اور اُنہیں چھوڑ رہے ہیں۔ براہ کرم دوبارہ کوشش کرنے سے قبل %{time_left} انتظار کریں۔" + topics_per_day: "آج آپ نئے ٹاپکس کی زیادہ سے زیادہ تعداد تک پہنچ گئے ہیں۔ براہ کرم دوبارہ کوشش کرنے سے قبل %{time_left} انتظار کریں۔" + pms_per_day: "آج آپ نئے پیغامات کی زیادہ سے زیادہ تعداد تک پہنچ گئے ہیں۔ براہ کرم دوبارہ کوشش کرنے سے قبل %{time_left} انتظار کریں۔" + create_like: "آج آپ لائیکس کی زیادہ سے زیادہ تعداد تک پہنچ گئے ہیں۔ براہ کرم دوبارہ کوشش کرنے سے قبل %{time_left} انتظار کریں۔" + create_bookmark: "آج آپ بُکمارکس کی زیادہ سے زیادہ تعداد تک پہنچ گئے ہیں۔ براہ کرم دوبارہ کوشش کرنے سے قبل %{time_left} انتظار کریں۔" + edit_post: "آج آپ ترامیم کی زیادہ سے زیادہ تعداد تک پہنچ گئے ہیں۔ براہ کرم دوبارہ کوشش کرنے سے قبل %{time_left} انتظار کریں۔" + live_post_counts: "آپ بہت جلدی سے لائیو پوسٹ شمار کیلئے پوچھ رہے ہیں۔ براہ کرم دوبارہ کوشش کرنے سے قبل %{time_left} انتظار کریں۔" + unsubscribe_via_email: "آج آپ ای میل کے ذریعہ رکنیت ختم کرنے کی زیادہ سے زیادہ تعداد تک پہنچ گئے ہیں۔ براہ کرم دوبارہ کوشش کرنے سے قبل %{time_left} انتظار کریں۔" + topic_invitations_per_day: "آج آپ ٹاپک کیلئے دعوت ناموں کی زیادہ سے زیادہ تعداد تک پہنچ گئے ہیں۔ براہ کرم دوبارہ کوشش کرنے سے قبل %{time_left} انتظار کریں۔" + hours: + one: "1 گھنٹا" + other: "%{count} گھنٹے" + minutes: + one: "1 منٹ" + other: "%{count} منٹس" + seconds: + one: "1 سیکنڈ" + other: "%{count} سیکنڈ" + datetime: + distance_in_words: + half_a_minute: "< 1منٹ" + less_than_x_seconds: + one: "< 1سیکنڈ" + other: "< %{count}سیکنڈ" + x_seconds: + one: "1سیکنڈ" + other: "%{count}سیکنڈ" + less_than_x_minutes: + one: "< 1منٹ " + other: "< %{count}منٹ " + x_minutes: + one: "1منٹ" + other: "%{count}منٹ " + about_x_hours: + one: "1گھنٹا" + other: "%{count}گھنٹے" + x_days: + one: "1دن" + other: "%{count}دن" + about_x_months: + one: "1ماہ" + other: "%{count}مہینے" + x_months: + one: "1ماہ" + other: "%{count}مہینے" + about_x_years: + one: "1سال" + other: "%{count}سال" + over_x_years: + one: "> 1سال" + other: "> %{count}سال" + almost_x_years: + one: "1سال" + other: "%{count}سال" + distance_in_words_verbose: + half_a_minute: "ابھی" + less_than_x_seconds: + one: "ابھی" + other: "ابھی" + x_seconds: + one: "1 سیکنڈ قبل" + other: "%{count} سیکنڈ قبل" + less_than_x_minutes: + one: "قبل از 1 منٹ سے بھی کم" + other: "قبل از %{count} منٹ" + x_minutes: + one: "1 منٹ قبل" + other: "%{count} منٹ قبل" + about_x_hours: + one: "قبل از 1 گھنٹہ" + other: " %{count} گھنٹے قبل" + x_days: + one: "قبل از 1 دن" + other: "%{count} دن قبل" + about_x_months: + one: "تقریباً 1 ماہ قبل" + other: "تقریباً %{count} مہینے قبل" + x_months: + one: "1 ماہ قبل" + other: " %{count} مہینے قبل " + about_x_years: + one: "تقریباً 1 سال قبل" + other: "تقریباً %{count} سال قبل" + over_x_years: + one: "1 سال سے زیادہ پہلے" + other: "%{count} سال سے زیادہ پہلے" + almost_x_years: + one: "تقریباً 1 سال قبل" + other: "تقریباً %{count} سال قبل" + password_reset: + no_token: "معذرت، وہ پاسورڈ تبدیل کرنے کا لِنک بہت پرانا ہے۔ لاگ ان کے بٹن کو منتخب کریں اور ایک نیا لِنک حاصل کرنے کیلئے 'میں اپنا پاسورڈ بھول گیا' کا استعمال کریں۔" + choose_new: "نیا پاسورڈ منتخب کریں" + choose: "پاسورڈ منتخب کریں" + update: 'پاسورڈ اَپ ڈیٹ کریں' + save: 'پاسورڈ رکھیں' + title: 'پاسورڈ رِی سَیٹ کریں' + success: "آپ نے کامیابی سے اپنا پاسورڈ تبدیل کر لیا اور اب آپ لاگ ان ہوے وے ہیں۔" + success_unapproved: "آپ نے کامیابی سے اپنا پاسورڈ تبدیل کر لیا۔" + email_login: + invalid_token: "معذرت، وہ بذریعہ ای میل لاگ اِن لِنک بہت پرانا ہے۔ لاگ ان کے بٹن کو منتخب کریں اور ایک نیا لِنک حاصل کرنے کیلئے 'میں اپنا پاسورڈ بھول گیا' کا استعمال کریں۔" + title: "بذریعہ ای میل لاگ اِن" + change_email: + confirmed: "آپ کا اِی میل اپڈیٹ کر دیا کیا ہے۔" + please_continue: "%{site_name} پر جاری رکھیں" + error: "آپ کا اِی میل ایڈریس تبدیل کرنے میں ایک خرابی کا سامنا کرنا پڑا۔ شاید یہ ایڈریس پہلے سے استعمال میں ہے؟" + error_staged: "آپ کا اِی میل تبدیل کرنے میں ایک خرابی کا سامنا کرنا پڑا۔ یہ ایڈریس پہلے سے ہی ایک سٹَیجڈ صارف کے استعمال میں ہے۔" + already_done: "معذرت، یہ تصدیقی لِنک اب درست نہیں ہے۔ شاید آپ کا اِی میل پہلے ہی بدل چکا تھا؟" + authorizing_old: + title: "اپنے موجودہ اِی میل ایڈریس کی تصدیق کرنے کیلئے شکریہ" + description: "اب ہم تصدیق کیلئے آپ کے نئے ایڈریس پر اِی میل کر رہے ہیں۔" + activation: + action: "اپنا اکاؤنٹ فعال کرنے کیلئے یہاں کلک کریں" + already_done: "معذرت، یہ اکاؤنٹ تصدیق لنک اب درست نہیں ہے۔ شاید آپ کا اکاؤنٹ پہلے ہی سے فعال ہے؟" + please_continue: "آپ کا نیا اکاؤنٹ تصدیق کر لیا گیا ہے؛ آپ کو ہَوم پیج پر ری ڈائرَیکٹ کیا جائے گا۔" + continue_button: "%{site_name} پر جاری رکھیں" + welcome_to: "%{site_name} پر خوش آمدید!" + approval_required: "اِس فورم تک رسائی حاصل کرنے سے پہلے ایک ماڈریٹر کو آپ کا نیا اکاؤنٹ دستی طور پر منظور کرنا ضروری ہے۔ جب آپ کا اکاؤنٹ منظور ہوجائے گا تو آپ کو ایک اِی میل مل جائے گی!" + missing_session: "ہم یہ پتہ نہیں لگا سکتے کہ آپ کا اکاؤنٹ بن چکا تھا کہ نہیں، براہ مہربانی یقینی بنائیں کہ کُوکِیز فعال ہوں۔" + activated: "معذرت، یہ اکاؤنٹ پہلے ہی سے چالو کر دیا گیا ہے۔" + admin_confirm: + title: "توثیق ایڈمن اکاؤنٹ" + description: "کیا آپ واقعی %{target_username} کو ایک ایڈمِنِسٹریٹر بننا چاہتے ہیں؟" + grant: "عطا ایڈمن رسائی" + complete: "%{target_username} اب ایک ایڈمِنِسٹریٹر ہے۔" + back_to: "%{title} پر واپس جائیں" + post_action_types: + off_topic: + title: 'موضوع سے ہٹ کر' + description: 'موجودہ بحث جو کہ عنوان اور پہلی پوسٹ سے واضح ہوتی ہے، یہ پوسٹ اُس سے متعلق نہیں ہے، اور غالباً کہیں اور منتقل ہوجانی چاہئیے۔' + short_description: 'بحث سے متعلق نہیں' + long_form: 'اِس کو موضوع سے ہٹ کر ہونے کے طور پر فلَیگ کیا گیا' + spam: + title: 'سپَیم' + description: 'یہ پوسٹ ایک اشتہار، یا وَینڈَلِزم ہے۔ یہ مفید یا موجودہ موضوع کے متعلقہ نہیں ہے۔' + short_description: 'یہ ایک اشتہار، یا وَینڈَلِزم ہے' + long_form: 'اِس کو سپَیم ہونے کے طور پر فلَیگ کیا گیا' + email_title: '"%{title}" کو سپَیم کے طور پر فلَیگ کیا گیا' + email_body: "%{link}\n\n%{message}" + inappropriate: + title: 'نامناسب' + description: 'اِس پوسٹ میں ایسا مواد شامل ہے جو ایک مناسب شخص جارحانہ، غیر مہذب، یا ہماری کمیونٹی کے قواعد و ضوابط کے خلاف سمجھے گا۔' + short_description: 'ہماری کمیونٹی کے قواعد و ضوابط کی خلاف ورزی' + long_form: 'اِس کو نامناسب ہونے کے طور پر فلَیگ کیا گیا' + notify_user: + title: '@{{username}} کو ایک پیغام بھیجیں' + description: 'میں اِس شخص سے اُس کی پوسٹ کے بارے میں براہ راست اور ذاتی طور پر بات کرنا چاہتا ہوں۔' + short_description: 'میں اِس شخص سے اُس کی پوسٹ کے بارے میں براہ راست اور ذاتی طور پر بات کرنا چاہتا ہوں۔' + long_form: 'پیغام دیا گیا صارف' + email_title: 'آپ کی "%{title}" میں پوسٹ' + email_body: "%{link}\n\n%{message}" + notify_moderators: + title: " کچھ اور" + description: 'اِس پوسٹ پر اسٹاف کی توجہ درکار ہے جس کی وجہ مندرجہ بالا درج وجوہات میں شامل نہیں ہے۔' + short_description: 'اسٹاف کی توجہ کسی اور وجہ کی بناہ پر درکار ہے' + long_form: 'اِس کو اسٹاف کی توجہ کیلئے فلَیگ کیا گیا' + email_title: '"%{title}" میں ایک پوسٹ پر اسٹاف کی توجہ درکار ہے' + email_body: "%{link}\n\n%{message}" + bookmark: + title: 'بُک مارک' + description: 'اِس پوسٹ کو بُک مارک کریں' + short_description: 'اِس پوسٹ کو بُک مارک کریں' + long_form: 'اِس پوسٹ کو بُک مارک کیا' + like: + title: 'لائیک' + description: ' اِس پوسٹ کو لائیک کریں' + short_description: ' اِس پوسٹ کو لائیک کریں' + long_form: 'اِس کو لائیک کیا' + vote: + title: 'ووٹ' + description: 'اِس پوسٹ کیلیے ووٹ کریں' + short_description: 'اِس پوسٹ کیلیے ووٹ کریں' + long_form: 'اِس پوسٹ کے لیے ووٹ کیا' + user_activity: + no_default: + self: "ابھی تک آپ کی کوئی سرگرمی نہیں ہے." + others: "کوئی سرگرمی نہیں۔" + no_bookmarks: + self: "آپ کے پاس کوئی بُک مارک کی ہوئی پوسٹس نہیں ہیں، پوسٹس بُک مارک کرنے سے آپ بعد میں اُن تک آسانی سے رسائی حاصل کرپاتے ہیں۔" + others: "کوئی بُک مارکس نہیں" + no_likes_given: + self: "آپ نے کوئی پوسٹس لائیک نہیں کیں۔" + others: "کوئی لائیک کردہ پوسٹس نہیں۔" + no_replies: + self: "آپ نے کسی بھی پوسٹ پر جواب نہیں دیا ہے۔" + others: "کوئی جوابات نہیں۔" + topic_flag_types: + spam: + title: 'سپَیم' + description: 'یہ ٹاپک ایک اشتہار ہے۔ یہ اِس سائٹ کیلئے مفید یا اِس کے متعلقہ نہیں ہے، لیکن نوعیت میں پروموشنل ہے۔' + long_form: 'اِس کو سپَیم کے طور پر فلَیگ کیا گیا' + inappropriate: + title: 'نامناسب' + description: 'اِس ٹاپک میں ایسا مواد شامل ہے جو ایک مناسب شخص جارحانہ، غیر مہذب، یا ہماری کمیونٹی کے قواعد و ضوابط کے خلاف سمجھے گا۔' + long_form: 'اِس کو نامناسب ہونے کے طور پر فلَیگ کیا گیا' + notify_moderators: + title: "کچھ اور" + description: 'اِس ٹاپک کو، قواعد و ضوابط، سروس کی شرائط، یا مندرجہ بالا درج وجوہات کے علاوہ، کے مطابق اسٹاف کی عام توجہ کی ضرورت ہے۔' + long_form: 'اِس کو ماڈریٹر کی توجہ کیلئے فلَیگ کیا گیا' + email_title: 'ٹاپک "%{title}" پر اسٹاف کی توجہ درکار ہے' + email_body: "%{link}\n\n%{message}" + flagging: + you_must_edit: '

    آپ کی پوسٹ کمیونٹی کی طرف سے فلَیگ کی گئی تھی۔ براہ مہربانی اپنے پیغامات دیکھیے۔

    ' + user_must_edit: '

    یہ پوسٹ کمیونٹی کی طرف سے فلَیگ کی گئی تھی اور عارضی طور پر چھپا دی گئی ہے۔

    ' + archetypes: + regular: + title: "عام ٹاپک" + banner: + title: "بینر ٹاپک" + message: + make: "یہ ٹاپک اب ایک بَینر ہے۔ یہ ہر صفحے کے سب سے اوپر دکھایا جائے گا جب تک صارف اِسے برخاست نہیں کر دیتا۔" + remove: "یہ ٹاپک اب بینر نہیں ہے۔ یہ اب ہر صفحے کے سب سے اوپر دکھایا نہیں جائے گا۔" + unsubscribed: + title: "غیر سَبسکرائب شدہ!" + description: "%{email} کو غیر سَبسکرائب کر دیا گیا ہے۔ اپنی ای میل کی ترتیبات کو تبدیل کرنے کیلئے اپنی صارفی ترجیحات پر جائیں۔" + topic_description: "%{link} پر دوبارہ سَبسکرائب کرنے کیلئے، ٹاپک کے نچلے حصے یا دائیں جانب اطلاعات کنٹرول کا استعمال کریں۔" + unsubscribe: + title: "غیر سَبسکرائب" + stop_watching_topic: "اس ٹاپک کو دیکھنا چھوڑ دیں، %{link}" + mute_topic: "اِس ٹاپک کیلئے تمام اطلاعات کو خاموش کر دیں، %{link}" + unwatch_category: "%{category} میں تمام ٹاپکس کو دیکھنا چھوڑ دیں" + mailing_list_mode: "مَیلِنگ لِسٹ مَوڈ بند کریں" + disable_digest_emails: "مجھے خلاصہ ای میلزبھیجنا روک دیں" + all: "مجھے %{sitename} سے کوئی میل نہ بھیجیں" + different_user_description: "جس صارف کو ہم نے اِی میل کی، آپ اُس سے مختلف صارف کے طور پر لاگ اِن ہیں۔ براہ مہربانی لاگ آوٹ کریں، یا گمنام مَوڈ میں داخل ہوں، اور دوبارہ کوشش کریں۔" + not_found_description: "معذرت، ہم اِس غیر سَبسکرائب کو تلاش نہ کرسکے۔ ممکن ہے کہ آپ کے ای میل میں لِنک کی میعاد ختم ہوگئی ہے؟" + log_out: "لاگ آُوٹ" + user_api_key: + title: "ایپلیکیشن ایکسَیس کی اجازت دیں" + authorize: "اجازت دیں" + read: "رِیڈ" + read_write: "رِیڈ/رائیٹ" + description: "\"%{application_name}\" آپ کے اکاؤنٹ سے مندرجہ ذیل رسائی کی درخواست کر رہا ہے:" + no_trust_level: "معذرت، صارف API تک رسائی حاصل کرنے کیلئے آپ کے پاس ضروری ٹرسٹ لَیول نہیں ہے" + generic_error: "معذرت، ہم صارف API کیز جاری کرنے سے قاصر ہیں، یہ خصوصیت سائٹ کے ایڈمن کی طرف سے غیر فعال کی گئی ہوسکتی ہے" + scopes: + message_bus: "لائیو اپ ڈیٹس" + notifications: "اطلاعات پڑھیں اور ہٹائیں" + push: "بیرونی سروِسوں پر اطلاعات بھیجیں" + session_info: "صارف سیشن کی معلومات پڑھیں" + read: "تمام رِیڈ" + write: " تمام رائیٹ" + reports: + visits: + title: "صارف وِزِٹس" + xaxis: "دن" + yaxis: "وِزِٹس کی تعداد" + signups: + title: "نئے صارفین" + xaxis: "دن" + yaxis: "نئے صارفین کی تعداد" + profile_views: + title: "صارف پروفائل وِیوز" + xaxis: "دن" + yaxis: "دیکھے گئے صارفین پروفائلز کی تعداد" + topics: + title: "ٹاپک" + xaxis: "دن" + yaxis: "نئے ٹاپکس کی تعداد" + posts: + title: "پوسٹس" + xaxis: "دن" + yaxis: "نئی پوسٹس کی تعداد" + likes: + title: "لائیکس" + xaxis: "دن" + yaxis: "نئے لائیکس کی تعداد" + flags: + title: "فلَیگز" + xaxis: "دن" + yaxis: "فلَیگز کی تعداد" + bookmarks: + title: "بُک مارکس" + xaxis: "دن" + yaxis: "نئے بُک مارکس کی تعداد" + starred: + title: "ستارہ شُدہ" + xaxis: "دن" + yaxis: "نئے ستارہ شُدہ ٹاپکس کی تعداد" + users_by_trust_level: + title: "صارفین فی ٹرسٹ لَیول" + xaxis: "ٹرسٹ لَیول" + yaxis: "صارفین کی تعداد" + emails: + title: "بھیجی گئیں اِی میل" + xaxis: "دن" + yaxis: "اِی میل کی تعداد" + user_to_user_private_messages: + title: "صارف-سے-صارف" + xaxis: "دن" + yaxis: "پیغامات کی تعداد" + system_private_messages: + title: "سِسٹَم" + xaxis: "دن" + yaxis: "پیغامات کی تعداد" + moderator_warning_private_messages: + title: "ماڈریٹر انتباہ" + xaxis: "دن" + yaxis: "پیغامات کی تعداد" + notify_moderators_private_messages: + title: "ماڈریٹرز کو مطلع کریں" + xaxis: "دن" + yaxis: "پیغامات کی تعداد" + notify_user_private_messages: + title: "صارف کو مطلع کریں" + xaxis: "دن" + yaxis: "پیغامات کی تعداد" + top_referrers: + title: "ٹاپ تجویز کنندگان" + xaxis: "صارف" + num_clicks: "کلِکس" + num_topics: "ٹاپک" + top_traffic_sources: + title: "ٹریفک کے ٹاپ ذرائع" + xaxis: "ڈَومَین" + num_clicks: "کلِکس" + num_topics: "ٹاپک" + num_users: "صارفین" + top_referred_topics: + title: "ٹاپ تجویز کردہ ٹاپک" + xaxis: "ٹاپک" + num_clicks: "کلِکس" + page_view_anon_reqs: + title: "گمنام" + xaxis: "دن" + yaxis: "صفحہ کے گمنام ملاحظات" + page_view_logged_in_reqs: + title: "لاگڈ اِن" + xaxis: "دن" + yaxis: "صفحہ کے لاگڈ اِن ملاحظات" + page_view_crawler_reqs: + title: "وَیب کرالرز" + xaxis: "دن" + yaxis: "صفحہ کے وَیب کرالر ملاحظات" + page_view_total_reqs: + title: "کُل" + xaxis: "دن" + yaxis: "صفحہ کے کُل ملاحظات" + page_view_logged_in_mobile_reqs: + title: "صفحہ کے لاگڈ اِن ملاحظات" + xaxis: "دن" + yaxis: "صفحہ کے موبائل لاگڈ اِن ملاحظات" + page_view_anon_mobile_reqs: + title: "صفحہ گمنام ملاحظات" + xaxis: "دن" + yaxis: "صفحہ موبائل گمنام ملاحظات" + http_background_reqs: + title: "پس منظر" + xaxis: "دن" + yaxis: "لائیو اَپ ڈیٹ اور ٹریکنگ کیلئے استعمال ہونے والی درخواستیں" + http_2xx_reqs: + title: "سٹَیٹَس 2xx (OK)" + xaxis: "دن" + yaxis: "کامیاب درخواستیں (سٹَیٹَس 2xx)" + http_3xx_reqs: + title: "HTTP 3xx (ریڈائرَیکٹ)" + xaxis: "دن" + yaxis: "ریڈائرَیکٹ کردہ درخواستیں (سٹَیٹَس 3xx)" + http_4xx_reqs: + title: "HTTP 4xx (کلائنٹ کی خرابی)" + xaxis: "دن" + yaxis: "کلائنٹ خرابیاں (سٹَیٹَس 4xx)" + http_5xx_reqs: + title: "HTTP 5xx (سرور کی خرابی)" + xaxis: "دن" + yaxis: "سرور خرابیاں (سٹَیٹَس 5xx)" + http_total_reqs: + title: "کُل" + xaxis: "دن" + yaxis: "کُل درخواستیں" + time_to_first_response: + title: "پہلے جواب تک کا وقت" + xaxis: "دن" + yaxis: "اوسطً وقت (گھنٹے)" + topics_with_no_response: + title: "بۃلا جواب والے ٹاپک" + xaxis: "دن" + yaxis: "کُل" + mobile_visits: + title: "صارف وِزِٹس" + xaxis: "دن" + yaxis: "وِزِٹس کی تعداد" + dashboard: + rails_env_warning: "آپ کا سرور %{env} مَوڈ میں چل رہا ہے۔" + host_names_warning: "آپ کی config/database.yml فائل ڈیفالٹ localhost نام استعمال کر رہا ہے۔ اِسے اپنے سائیٹ ہوسٹ کے نام پر اپ ڈیٹ کریں۔" + gc_warning: 'آپ کا سرور ڈیفالٹ روبی گاربَیج کولیکشن پیرامیٹرز کا استعمال کر رہا ہے، جو آپ کو بہترین کارکردگی نہیں دے گا۔ کارکردگی کی ٹیوننگ پر اس ٹاپک کو پڑھیں: ڈِسکورس کیلئے روبی اور ریلز کی ٹیوننگ۔' + 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) کے ساتھ سائن اپ اور لاگ اِن کی اجازت ہو، لیکن کلائنٹ آئی ڈی اور سیکرٹ وَیلِیوز مقرر نہیں کیے گئے ہیں۔ سائٹ ترتیبات پر جائیں اور ترتیبات کو اَپ ڈیٹ کریں۔ مزید جاننے کے لئے یہ گائیڈ ملاحظہ کریں۔' + s3_config_warning: 'سرور کو s3 پر فائلوں کو اَپ لوڈ کرنے کیلئے ترتیب دیا گیا ہے، لیکن کم از کم ایک درج ذیل ترتیب سَیٹ نہیں کی گئی ہے: s3_access_key_id, s3_secret_access_key, s3_use_iam_profile, or s3_upload_bucket۔ سائٹ ترتیبات پر جائیں اور ترتیبات کو اَپ ڈیٹ کریں۔ مزید جاننے کے لئے "S3 پر تصویر اپ لوڈز کیسے سَیٹ کریں؟" ملاحظہ کریں۔' + s3_backup_config_warning: 'سرور کو s3 پر بیک اَپس اَپ لوڈ کرنے کیلئے ترتیب دیا گیا ہے، لیکن کم از کم ایک درج ذیل ترتیب سَیٹ نہیں کی گئی ہے: s3_access_key_id, s3_secret_access_key, s3_use_iam_profile, or s3_backup_bucket۔ سائٹ ترتیبات پر جائیں اور ترتیبات کو اَپ ڈیٹ کریں۔ مزید جاننے کے لئے "S3 پر تصویر اپ لوڈز کیسے سَیٹ کریں؟" ملاحظہ کریں۔' + image_magick_warning: 'سرور کو بڑی تصاویر کے تھَمب نَیل تخلیق کرنے کیلئے ترتیب دیا گیا ہے، لیکن ImageMagick اِنسٹال نہیں ہے۔ اپنے پسندیدہ پیکیج مینیجر کا استعمال کرکے ImageMagick اِنسٹال کریں یا تازہ ترین ورژن ڈاؤن لوڈ کریں۔' + failing_emails_warning: 'ناکام ہونے والی %{num_failed_jobs} اِی مَیل جابز موجود ہیں۔ اپنا app.yml چیک کریں اور یہ یقینی بنائیں کہ میل سرور کی ترتیبات درست ہیں۔ Sidekiq میں ناکام جابز ملاحظہ کریں۔' + subfolder_ends_in_slash: "آپ کا سب-فولڈر سیٹ اپ غلط ہے؛ DISCOURSE_RELATIVE_URL_ROOT ایک سلَیش میں ختم ہوتا ہے۔" + email_polling_errored_recently: + one: "گزشتہ 24 گھنٹوں میں ای میل پولِنگ نے ایک خرابی دکھائی ہے۔ مزید تفصیلات کیلئے لاگز کا ملاحضہ کریں۔" + other: "گزشتہ 24 گھنٹوں میں ای میل پولِنگ نے %{count} خرابیاں دکھائی ہیں۔ مزید تفصیلات کیلئے لاگز کا ملاحضہ کریں۔" + missing_mailgun_api_key: "سرور کو بذریعہ مَیلگَن ای میل بھیجنے کیلئے ترتیب دیا گیا ہے، لیکن آپ نے وَیب ھُوک٘ پیغامات کی توثیق کرنے کیلئے API کلید فراہم نہیں کی ہے۔" + bad_favicon_url: "فَیوِکان لوڈ نہیں ہو رہا ہے۔ سائٹ ترتیبات میں اپنی favicon_url کی ترتیبات چیک کریں۔" + poll_pop3_timeout: "POP3 سرور کے ساتھ کنِکشن کا وقت ختم ہوا جا رہا ہے۔ آنے والی ای میل حاصل نہیں کی جاسکی۔ براہ مہربانی اپنی POP3 ترتیبات اور سروس فراہم کرنے والے کو چیک کریں۔" + poll_pop3_auth_error: "POP3 سرور کے ساتھ کنِکشن ایک اوَتھَینٹیکیشن خرابی سے ناکام ہو رہا ہے۔ آنے والی ای میل حاصل نہیں کی جاسکی۔ براہ مہربانی اپنی POP3 ترتیبات کو چیک کریں۔" + force_https_warning: "آپ کی ویب سائٹ SSL استعمال کر رہی ہے۔ لیکن `force_https` ابھی تک آپ کی سائٹ ترتیبات میں فعال نہیں ہے۔" + site_settings: + censored_words: "الفاظ جو خود بخود ■■■■ سے تبدیل ہوجائیں گے" + delete_old_hidden_posts: "کوئی چھپی ہوئی پوسٹ جو 30 دن سے زائد عرصہ سے چھپا رکھی ہو اُس کو خود کار طریقے سے حذف کر دیں۔" + default_locale: "اِس ڈِسکورس سَیٹ اپ کی پہلے سے طہ شدہ زبان" + allow_user_locale: "صارفین کو اپنی زبان انٹرفیس ترجیح کو منتخب کرنے کی اجازت دیں" + set_locale_from_accept_language_header: "گمنام صارفین کیلئے اِنٹرفیس زبان اُن کے وَیب براؤزر کے لَینگوَیج ہیڈرز سے سَیٹ کریں۔ (تجرباتی، گمنام کَیشے کے ساتھ کام نہیں کرتا)" + support_mixed_text_direction: "مخلوط بائیں-سے-دائیں اور دائیں-سے-بائیں ٹیکسٹ سمتوں کی اجازت دیں۔" + min_post_length: "حروف میں پوسٹ کی کم از کم لمبائی" + min_first_post_length: "حروف میں پہلی پوسٹ (ٹاپک متن) کی کم از کم لمبائی" + min_personal_message_post_length: "حروف میں پیغامات کیلئے پوسٹ کی کم از کم لمبائی" + max_post_length: "حروف میں پوسٹ کی زیادہ سے زیادہ لمبائی" + topic_featured_link_enabled: "ٹاپکس کے ساتھ لِنک پوسٹ کرنا فعال کریں۔" + show_topic_featured_link_in_digest: "ڈائجسٹ ای میل میں نمایاں ٹاپک کا لِنک دکھائیں۔" + min_topic_title_length: "حروف میں ٹاپک کے عنوان کی کم از کم لمبائی" + max_topic_title_length: "حروف میں ٹاپک کے عنوان کی زیادہ سے زیادہ لمبائی" + min_personal_message_title_length: "حروف میں ایک پیغام کیلئے عنوان کی کم از کم لمبائی" + max_emojis_in_title: "ٹاپک عنوان میں اِیمَوجیوں کی زیادہ سے زیادہ تعداد" + min_search_term_length: "حروف میں درست سرچ ٹَرم کی کم از کم لمبائی" + search_prefer_recent_posts: "اگر آپ کے بڑے فورم پر سرچ سست ہے، تو یہ آپشن پہلے حالیہ پوسٹس کے ایک اِنڈَیکس پر کوشش کرتا ہے" + search_recent_posts_size: "اِنڈَیکس میں کتنی حالیہ پوسٹس رکھی جائیں" + log_search_queries: "صارفین کی طرف سے سرچ قُوَیریز کو لاگ کریں" + search_query_log_max_size: "رکھی جانے والی سرچ قُوَیریز کی زیادہ سے زیادہ تعداد" + allow_uncategorized_topics: "ٹاپکس کو بغیر کسی زُمرہ کے تخلیق ہونے کی اجازت دیں۔ انتباہ: اگر کوئی بِلا زُمرہ ٹاپکس موجود ہوں، تو اِس کو بند کرنے سے پہلے آپ کو اُنہیں دوبارہ کسی زُمرہ میں ڈالنا ہوگا۔" + allow_duplicate_topic_titles: "ایک جیسے، نقل عنوانات کے ساتھ ٹاپکس کی اجازت دیں۔" + unique_posts_mins: "ایک ہی مواد والی دوبارہ پوسٹ صارف کتنے منٹ بعد بنا سکتا ہے" + educate_until_posts: "جب صارف اپنی پہلی (ن) نئی پوسٹس کو ٹائپ کرنا شروع کرے، تو کمپوزر میں نیا صارف تعلیمی پینل کا پاپ-اپ دکھائیں۔" + title: "اس سائٹ کا نام، جیسا کہ عنوان ٹَیگ میں استعمال ہوتا ہے۔" + site_description: "اِس سائٹ کو ایک جملہ میں بیان کریں، جیسا کہ مَیٹا وضاحتی ٹَیگ میں استعمال کیا جاتا ہے۔" + contact_email: "اِس سائٹ کیلئے ذمہ دار اہم کانٹیکٹ کا ای میل ایڈریس۔ اہم اطلاعات کے لئے استعمال کیا جاتا ہے، اور اُس کے ساتھ ساتھ فوری طور کے معاملات کیلئے /about رابطہ فارم پر۔" + contact_url: "اِس سائٹ کے لئے رابطہ URL۔ فوری طور کے معاملات کیلئے /about رابطہ فارم پر استعمال کیا جاتا ہے۔" + crawl_images: "صحیح چوڑائی اور اونچائی والے طول و عرض داخل کرنے کیلئے ریمَوٹ URL سے تصاویر حاصل کریں۔" + download_remote_images_to_local: "ڈاؤن لوڈ کرکے ریمَوٹ تصاویر کو مقامی تصاویر میں تبدیل کریں؛ اِس طرح سے ٹوٹی ہوئی تصاویر کو روکا جا سکتا ہے۔" + download_remote_images_threshold: "مقامی طور پر ریمَوٹ تصاویر کو ڈاؤن لَوڈ کرنے کیلئے ڈِسک میں کم از کم جگہ (فیصد میں)" + download_remote_images_max_days_old: "اُن پوسٹس کیلئے ریمَوٹ تصاویر ڈاؤن لوڈ نہ کریں جو ن دنوں سے زیادہ پرانی ہوں۔" + disabled_image_download_domains: "ریمَوٹ تصاویر اِن ڈومینز سے کبھی ڈاؤن لوڈ نہیں ہوں گی۔ پائپ کے ساتھ الگ کی گئی فہرست۔" + editing_grace_period: "پوسٹنگ کے بعد (ن) سیکنڈ تک، نئی ترمیم پوسٹ کی ہسٹری میں ایک نیا ورژن نہیں بنائے گا۔" + editing_grace_period_max_diff: "ترمیم کی رعایتی مدت کے دوران حروف تبدیلیوں کی زیادہ سے زیادہ تعداد، اگر اِس سے زیادہ تبدیل کیے جائیں تو ایک اور پوسٹ رَوِیژن سٹور کریں (ٹرسٹ لَیول 0 اور 1)" + editing_grace_period_max_diff_high_trust: "ترمیم کی رعایتی مدت کے دوران حروف تبدیلیوں کی زیادہ سے زیادہ تعداد، اگر اِس سے زیادہ تبدیل کیے جائیں تو ایک اور پوسٹ رَوِیژن سٹور کریں (ٹرسٹ لَیول 2 اور اوپر)" + staff_edit_locks_post: "پوسٹس کو ترمیم کرنے سے روک دیا جائے گا اگر وہ اسٹاف کے اراکین کی طرف سے ترمیم کی گئی ہوں" + post_edit_time_limit: "مصنف اشاعت کے بعد (ن) منٹ تک اپنی پوسٹ میں ترمیم یا اسے خارج کرسکتے ہیں۔ ہمیشہ کیلئے 0 پر سیٹ کریں۔" + edit_history_visible_to_public: "سب کو ایک ترمیم شدہ پوسٹ کے پچھلے ورژن دیکھنے کی اجازت دیں۔ غیر فعال ہونے پر، صرف اسٹاف ممبران دیکھ سکتے ہیں۔" + delete_removed_posts_after: "مصنف کی طرف سے ہٹائی گئی پوسٹس خود کار طریقے سے (ن) گھنٹوں کے بعد حذف کردی جائیں گی۔ اگر 0 پر سیٹ ہو، تو پوسٹس کو فوری طور پر حذف کردیا جائے گا۔" + max_image_width: "پوسٹ میں تصاویر کی زیادہ سے زیادہ تھَمب نَیل چوڑائی" + max_image_height: "پوسٹ میں تصاویر کی زیادہ سے زیادہ تھَمب نَیل اونچائی" + fixed_category_positions: "اگر باکس چیک شدہ ہو تو، آپ زُمرہ جات کو ایک مقررہ سلسلہ کے حساب سے اُن کی درجہ بندی کرسکیں گے۔ اگر چیک شدہ نہ ہو تو، سرگرمیوں کی تعداد کے مطابق زُمرہ جات درج کیے جاتے ہیں۔" + fixed_category_positions_on_create: "اگر باکس چیک شدہ ہو تو، ٹاپک تخلیق ڈائیلاگ پر زُمرہ جات کی درجہ بندی کی ترتیب برقرار رکھی جائے گی (fixed_category_positions ضروری ہے)۔" + add_rel_nofollow_to_user_content: "اندرونی لِنکس (بالائی ڈومینز سمیت) کے علاوہ، تمام شائع کردہ صارف کے مواد پر رَیل nofollow شامل کریں۔ اگر آپ اس کو تبدیل کرتے ہیں تو، آپ کو تمام پوسٹس کو دوبارہ رِیبَیک کرنا ہوگا: \"rake posts:rebake\"" + exclude_rel_nofollow_domains: "اُن ڈومینز کی فہرست جن کے لنکس پر nofollow کو شامل نہیں کیا جانا چاہئے۔ example.com خود بخود sub.example.com کو بھی اجازت دیدے گا۔ وَیب کرالرز کو تمام مواد تلاش کرنے میں مدد دینے کیلئے آپ کو کم از کم، اِس سائٹ کا ڈومَین شامل کرلینا چاہئے۔ اگر آپ کی ویب سائٹ کے دوسرے حصے دوسرے ڈومینز پر ہیں، تو اُنہیں بھی شامل کر لیں۔" + post_excerpt_maxlength: "پوسٹ اقتباس / خلاصہ کی زیادہ سے زیادہ لمبائی۔" + show_pinned_excerpt_mobile: "موبائل وِیو میں پِن ہوے ٹاپکس پر اقتباس دکھائیں۔" + show_pinned_excerpt_desktop: "ڈیسک ٹاپ وِیو میں پِن ہوے ٹاپکس پر اقتباس دکھائیں۔" + post_onebox_maxlength: "ایک وَن باکسڈ ڈِسکورس پوسٹ کے حروف کی زیادہ سے زیادہ لمبائی۔" + onebox_domains_blacklist: "ڈومینز کی ایک فہرست جو کبھی بھی وَن باکسڈ نہیں کیے جائیں گے۔" + inline_onebox_domains_whitelist: "ڈومینز کی ایک فہرست جو چھوٹے فارم میں وَن باکسڈ کیے جائیں گے اگر وہ عنوان کے بغیر لنک کیے جائیں" + enable_inline_onebox_on_all_domains: "inline_onebox_domain_whitelist سائٹ ترتیب کو نظر انداز کریں اور تمام ڈَومینز پر وَن باکس کی اجازت دیں۔" + max_oneboxes_per_post: "ایک پوسٹ میں وَن باکس کی زیادہ سے زیادہ تعداد۔" + logo_url: "آپ کی سائٹ کے سب سے اوپر بائیں پر لوگو کی تصویر وسیع مستطیل شکل ہونی چاہئے۔ اگر خالی چھوڑ دیا گیا ہو تو سائٹ کا عنوان دکھایا جائے گا۔" + digest_logo_url: "آپ کی سائٹ کے ای میل خلاصہ کے اوپر استعبال ہونے والا متبادل کوگو۔ ایک وسیع مستطیل شکل ہونی چاہئے۔ ایک SVG تصویر نہیں ہونی چاہئے۔ اگر خالی چھوڑ دیا گیا ہو تو `logo_url` استعمال کیا جائے گا۔" + logo_small_url: "آپ کی سائٹ کے سب سے اوپر بائیں پر چھوٹا سی لوگو کی تصویر، ایک چکور شکل ہونی چاہئے، جب نیچے سکرول کیا جائے تو دکھائی دیتا ہے۔ اگر خالی چھوڑ دیا گیا ہو تو ایک ہَوم گلِف دکھایا جائے گا۔" + favicon_url: "آپ کی ویب سائٹ کیلئے ایک فَیوِکان، http://en.wikipedia.org/wiki/Favicon دیکھیے، ایک CDN پر صحیح کام کرنے کیلئے اِس کا png ہونا ضروری ہے" + mobile_logo_url: "آپکی سائٹ کے موبائل ورژن پر استعمال ہونے والا اپنی مرضی کا لوگو url۔ اگر خالی چھوڑ دیا گیا ہو تو `logo_url` استعمال کیا جائے گا۔ مثال: http://example.com/uploads/default/logo.png" + large_icon_url: "تصویر جو اینڈرائڈ پر لوگو/سپلیش تصویر کے طور پر استعمال ہو۔ تجویز شدہ سائز 512px/512px ہے۔" + apple_touch_icon_url: "اَیپل ٹَچ ڈِیوائیسِز پر استعمال ہونے والا آئکن۔ تجویز شدہ سائز 144px/144px ہے۔" + notification_email: "تمام ضروری سِسٹم ای میلز بھیجنے کیلئے استعمال ہونے والا from: ای میل ایڈریس۔ یہاں درج کردہ ڈومین کا SPF ،DKIM اور ریورس PTR ریکارڈ، ای میل کے پہنچنے کیلئے، صحیح طریقے سے مقرر ہونا لازمی ہے۔" + email_custom_headers: "اپنی مرضی کے ای میل ہیڈرز کی پائپ سے علیحدہ کردہ فہرست" + email_subject: "سٹینڈرڈ ای میلز کیلئے اپنی مرضی کا موضوع فارمَیٹ۔ دیکھیے https://meta.discourse.org/t/customize-subject-format-for-standard-emails/20801" + force_https: "صرف HTTPS استعمال کرنے کیلئے اپنی سائٹ کو مجبورکریں۔ انتباہ: جب تک آپ تصدیق نہ کر لیں کہ HTTPS مکمل طور پر سیٹ ہے اور ہر جگہ کام کر رہا ہے، اِس کو فعال نہ کریں! کیا آپ نے اپنا CDN، تمام سماجی لاگ ان، اور بیرونی لوگو / انحصارات کو چیک کر کے یہ یقینی بنا لیا ہے کہ وہ تمام HTTPS سے مطابقت رکھتے ہیں؟" + same_site_cookies: "سَیم سائٹ کے کُوکِیز کا استعمال کریں، وہ قابل براؤزرز (Lax یا Strict) پر تمام ویکٹر Cross Site Request Forgery کو ختم کر دیتے ہیں۔ انتباہ: Strict صرف اُن سائٹس پر کام کرے گا جو لاگ اِن پر مجبور کرتے ہیں اور SSO کا استعمال کرتے ہیں۔" + summary_score_threshold: "'اِس ٹاپک کا خلاصہ کریں' میں شامل ہونے کیلئے ایک پوسٹ کا کم از کم سکور" + summary_posts_required: "'اِس ٹاپک کا خلاصہ کریں' فعال ہونے سے پہلے ٹاپک میں پوسٹس کی کم از کم تعداد" + summary_likes_required: "'اِس ٹاپک کا خلاصہ کریں' فعال ہونے سے پہلے ٹاپک میں لائیکس کی کم از کم تعداد" + summary_percent_filter: "جب صارف 'اِس ٹاپک کا خلاصہ کریں' پر کلک کرتا ہے، تو سب سے اوپر % پوسٹس دکھائیں" + summary_max_results: "'اِس ٹاپک کا خلاصہ کریں' کی طرف سے لوٹائی جانے والی زیادہ سے زیادہ پوسٹس" + enable_personal_messages: "ٹرسٹ لَیول 1 (پیغامات بھیجنے کیلئے کم از کم ٹرسٹ لَیول کے ذریعہ ترتیب دے سکتے ہیں) والے صارفین کو پیغامات بنانے اور پیغامات کا جواب دینے کی اجازت دیں۔ نوٹ کریں کہ جوبھی ہو، اسٹاف ہمیشہ پیغامات بھیج سکتا ہے۔" + enable_system_message_replies: "صارفین کو سِسٹم پیغامات کا جواب دینے کی اجازت دیں، یہاں تک کہ اگر ذاتی پیغامات بھی غیر فعال ہوں" + enable_personal_email_messages: "ٹرسٹ لَیول 4 (پیغامات بھیجنے کیلئے کم از کم ٹرسٹ لَیول کے ذریعہ ترتیب دے سکتے ہیں) والے صارفین کو ذاتی ای میل پیغامات کی اجازت دیں۔ نوٹ کریں کہ جوبھی ہو، اسٹاف ہمیشہ پیغامات بھیج سکتا ہے۔" + enable_long_polling: "نوٹیفکیشن کیلئے مَیسج بس استعمال ہو رہا ہے، لانگ پولِنگ کا استعمال کیا جا سکتا ہے" + long_polling_base_url: "لانگ پولِنگ کیلئے استعمال ہونے والا بَیس URL (جب ایک CDN متحرک مواد فراہم کر رہا ہو، تو اِس کو اوریِجِن پُل پر سَیٹ کرنا یقینی بنائیں) مثال: http://origin.site.com" + long_polling_interval: "جب بھیجنے کیلئے کوئی ڈیٹا نہ ہو تو کلائینٹس کوجواب دینے سے پہلے سرور کو کتنا وقت انتظار کرنا چاہئے (صرف لاگ ہوئے صارفین)" + polling_interval: "جب لانگ پولِنگ نہ ہو، تو لاگ ہوئے کلائنٹس کو مِلی سیکنڈوں میں کتنی دفعہ پَول کرنا چاہئے" + anon_polling_interval: "گمنام کلائنٹس کو مِلی سیکنڈوں میں کتنی دفعہ پَول کرنا چاہئے" + background_polling_interval: "کلائنٹس کو مِلی سیکنڈوں میں کتنی دفعہ پَول کرنا چاہئے (جب وِنڈو پسِ منظر میں ہو)" + flags_required_to_hide_post: "فلَیگز کی تعداد جس کے بعد کسی پوسٹ کو خود کار طریقہ سے چھپا دیا اور صارف کو پیغام بھیج دیا جاتا ہے (کبھی نہیں کیلئے 0)" + cooldown_minutes_after_hiding_posts: "منٹوں کی تعداد جن کیلئے ایک صارف کو کمیونٹی فلَیگ بندی کے ذریعہ چھپائی گئی پوسٹ میں ترمیم کرنے سے پہلے انتظار کرنا لازمی ہے" + max_topics_in_first_day: "اُن کی پہلی پوسٹ بنانے کے بعد 24 گھنٹوں کی مدت میں ایک صارف کی طرف سے تخلیق کردہ ٹاپکس کی زیادہ سے زیادہ تعداد" + max_replies_in_first_day: "اُن کی پہلی پوسٹ بنانے کے بعد 24 گھنٹوں کی مدت میں ایک صارف کی طرف سے تخلیق کردہ جوابات کی زیادہ سے زیادہ تعداد" + tl2_additional_likes_per_day_multiplier: "اِس نمبر کے ساتھ ضرب کر کے ٹ.ل.2 (ممبر) کیلئے فی دن لائیکس کی حد میں اضافہ کریں" + tl3_additional_likes_per_day_multiplier: "اِس نمبر کے ساتھ ضرب کر کے ٹ.ل.3 (رَیگولر) کیلئے فی دن لائیکس کی حد میں اضافہ کریں" + tl4_additional_likes_per_day_multiplier: "اِس نمبر کے ساتھ ضرب کر کے ٹ.ل.4 (لِیڈر) کیلئے فی دن لائیکس کی حد میں اضافہ کریں" + num_spam_flags_to_silence_new_user: "اگر کسی نئے صارف کی پوسٹس کو num_users_to_silence_new_user مختلف صارفین سے اتنے سپَیم فلَیگز ملیں، تو اُن کی تمام پوسٹس کو چھپا اور مستقبل کی اشاعت کو روک دیں۔ غیر فعال کرنے کیلئے 0۔" + num_users_to_silence_new_user: "اگر کسی نئے صارف کی پوسٹس کو اتنے مختلف صارفین سے num_spam_flags_to_silence_new_user سپَیم فلَیگز ملیں، تو اُن کی تمام پوسٹس کو چھپا اور مستقبل کی اشاعت کو روک دیں۔ غیر فعال کرنے کیلئے 0۔" + num_tl3_flags_to_silence_new_user: "اگر کسی نئے صارف کی پوسٹس کو num_tl3_users_to_silence_new_user مختلف ٹرسٹ لَیول 3 والے صارفین سے اتنے فلَیگز ملیں، تو اُن کی تمام پوسٹس کو چھپا اور مستقبل کی اشاعت کو روک دیں۔ غیر فعال کرنے کیلئے 0۔" + num_tl3_users_to_silence_new_user: "اگر کسی نئے صارف کی پوسٹس کو اتنے مختلف ٹرسٹ لَیول 3 والے صارفین سے num_tl3_flags_to_silence_new_user فلَیگز ملیں، تو اُن کی تمام پوسٹس کو چھپا اور مستقبل کی اشاعت کو روک دیں۔ غیر فعال کرنے کیلئے 0۔" + notify_mods_when_user_silenced: "اگر صارف خود کار طریقے سے خاموش کر دیا جائے، تو تمام ماڈریٹرزکو ایک پیغام بھیجیں۔" + flag_sockpuppets: "اگر ایک نیا صارف اُسی IP ایڈریس سے کسی ٹاپک پر جواب دیتا ہے جس IP ایڈریس سے ایک دوسرے نئے صارف نے وہی ٹاپک شروع کیا تھا، تو اُن دونوں کی پوسٹس کو ممکنہ سپَیم کے طور پر فلَیگ کریں۔" + traditional_markdown_linebreaks: "مارکڈائون میں روایتی لائن وقفے کا استعمال کریں، جس میں ایک لائن وقفے کیلئے دو ٹریلنگ خالی جگہوں کی ضرورت ہوتی ہے۔" + enable_markdown_typographer: "پیراگرافوں کے ٹیکسٹ کا پڑھنا آسان بنانے کیلئے بنیادی چپھائی کے قوانین کا استعمال کریں، (c) (tm) وغیرہ کو علامات کے ساتھ تبدیل کردیں، کئی سوالیہ نشانوں کی تعداد کم ہوجائے اور اِسی طرح دیگر" + enable_markdown_linkify: "ٹیکسٹ جو ایک لنک کی طرح لگے اُسے خود کار طریقے سے ایک لنک کے طور پر دکھائیں: www.site.com اور http://site.com خود کار طریقے سے لِنک کر دیے جائیں گے" + markdown_linkify_tlds: "سب سے اوپر کی سطح کے ڈَومینز کی فہرست جو خود بخود لِنکس کے طور پر دکھائے جاتے ہیں" + post_undo_action_window_mins: "منٹوں کی تعداد جب تک صارفین کو ایک پوسٹ پر حالیہ کارروائیوں (لائیک، فلَیگ، وغیرہ) کو واپس کرلینے کی اجازت ہے۔" + must_approve_users: "سائٹ تک رسائی حاصل کرنے سے پہلے اسٹاف کو تمام نئے صارف اکاؤنٹس کو منظور کرنا ضروری ہے۔ انتباہ: کسی لائیو سائٹ کیلئے اِس کو فعال کرنے سے غیر اسٹاف صارفین کیلئے رسائی منسوخ ہو جائے گی!" + pending_users_reminder_delay: "ماڈریٹرز کو مطلع کریں اگر نئے صارفین اِس سے زیادہ گھنٹوں سے منظوری کے منتظر ہوں۔ اطلاعات کو غیر فعال کرنے کیلئے -1 سَیٹ کریں۔" + maximum_session_age: "آخری وزٹ سے ن گھنٹوں بعد تک صارف کو لاگڈ اِن رکھا جائے گا" + ga_universal_tracking_code: "گُوگل یونیوَرسل اَینَیلَیٹِکس (analytics.js) ٹرَیکِنگ کَوڈ کَوڈ، مثال: UA-12345678-9؛ دیکھیے http://google.com/analytics" + ga_universal_domain_name: "گُوگل یونیوَرسل اَینَیلَیٹِکس (analytics.js) ڈَومَین نام، مثال: mysite.com؛ دیکھیے http://google.com/analytics" + ga_universal_auto_link_domains: "گُوگل یونیوَرسل اَینَیلَیٹِکس (analytics.js) کراس ڈَومَین ٹرَیکِنگ فعال کریں۔ اِن ڈومینز کے باہر جانے والے لِنکس کے ساتھ کلائینٹ id شامل کر دی جائے گی۔ گُوگل کی کراس ڈَومَین ٹریکنگ گائیڈ ملاحظہ کریں۔" + gtm_container_id: "گُوگل ٹیگ مینیجر کی کنٹینر آئی ڈی. مثال: GTM-ABCDEF" + enable_escaped_fragments: "اگر کسی وَیب کرالر کا پتہ نہ لگے تو گُوگل اَیجَیکس-کرالِنگ API کا استعمال کریں۔ دیکھیے https://developers.google.com/webmasters/ajax-crawling/docs/learn-more" + allow_moderators_to_create_categories: "منتظمین کو نئے زُمرہ جات بنانے کی اجازت دیں" + cors_origins: "وہ اَوریجِن جن کیلئے کراس-اَوریجِن درخواستوں (CORS) کی اجازت ہے۔ ہراَوریجِن میں http:// یا https:// شامل ہونا ضروری ہے۔ CORS کو فعال کرنے کیلئے DISCOURSE_ENABLE_CORS کی وَیلِیو ٹرُو پر مقرر ہونا لاذمی ہے۔" + use_admin_ip_whitelist: "ایڈمن صرف اُس صورت میں لاگ ان کرسکتے ہیں اگر وہ ایسے IP ایڈریس میں ہیں جو اسکرین کردہ آئی پیز کی فہرست (ایڈمن > لاگز > اسکرین کردہ آئی پیز) میں بیان کیا گیا ہو۔" + blacklist_ip_blocks: "نجی IP بلاکس کی فہرست جو ڈِسکَورس کی طرف سے کبھی کرال نہیں ہونی چاہئیں" + whitelist_internal_hosts: "اندرونی ہَوسٹس کی فہرست جو ڈِسکَورس محفوظ طریقے سے وَن باکس اور دیگر مقاصد کیلئے کرال کر سکتا ہے" + allowed_iframes: "iframe src ڈَومَین کے سابقوں کی ایک فہرست جو ڈِسکَورس محفوظ طریقے سے پوسٹس میں شامل ہونے کی اجازت دے سکتا ہے" + top_menu: "اِس بات کا تعین کریں کہ ہَوم پیج نیویگیشن پر کونسی، اور کس ترتیب کے ساتھ اشیاء ظاہر ہوں۔ مثال کے طور پر تازہ ترین|نئی|بغیر پڑھی|زُمرہ جات|ٹاپ|پڑھ لیے گئے|شائع کیے|بُکمارکس" + post_menu: "اِس بات کا تعین کریں کہ پوسٹ مَینِیو پر کونسی، اور کس ترتیب کے ساتھ اشیاء ظاہر ہوں۔ مثال کے طور پر لائیک|ترمیم| فلَیگ|حذف|شئیر|بُکمارک|جواب" + post_menu_hidden_items: "پوسٹ مینیو میں ڈیفالٹ کے طور پرچھپائی جانے والی اشیاء جب تک کسی توسیعی ellipsis پر کلِک نہ کیا جائے۔" + share_links: "اِس بات کا تعین کریں کہ شیئر ڈائیلاگ پر کونسی، اور کس ترتیب کے ساتھ اشیاء ظاہر ہوں۔" + track_external_right_clicks: "بیرونی لِنکس جن پر دائیاں کلک کیا جائے اُن کو ٹریک کریں (مثال: نئے ٹیب میں کھولیں) ڈیفالٹ کے طور پر غیر فعال، کیونکہ یہ URLs کو رِیرائیٹ کر دیتا ہے" + site_contact_username: "ایک درست سٹاف صارف نام جس کی طرف سے تمام خود کار طریقے سے پیغامات بھیجے جائیں۔ اگر خالی چھوڑ دیا گیا ہو تو ڈیفالٹ سِسٹم اکاؤنٹ استعمال کیا جائے گا۔" + send_welcome_message: "تمام نئے صارفین کو فوری شروعات کے گائیڈ کے ساتھ خوش آمدید کا پیغام بھیجیں۔" + suppress_reply_directly_below: "پوسٹ پر وسیع ہو سکنے والا جوابات کی تعداد مت دکھائیں جب اِس پوسٹ کے براہ راست نیچے ایک ہی واحد جواب موجود ہو۔" + suppress_reply_directly_above: "پوسٹ پر وسیع ہو سکنے والا جس-کے-جواب-میں مت دکھائیں جب اِس پوسٹ کے براہ راست اوپر ایک ہی واحد جواب موجود ہو۔" + suppress_reply_when_quoting: "پوسٹ پر وسیع ہو سکنے والا جس-کے-جواب-میں مت دکھائیں جب پوسٹ میں جواب کا اقتباس شامل ہو۔" + max_reply_history: "جس-کے-جواب-میں توسیع کرتے وقت، توسیع کیے گئے جوابات کی زیادہ سے زیادہ تعداد" + topics_per_period_in_top_summary: "ڈیفالٹ کے طور پر ٹاپ ٹاپکس خلاصہ میں دکھائے گئے ٹاپ ٹاپکس کی تعداد۔" + topics_per_period_in_top_page: "توسیع کردہ ٹاپ ٹاپکس 'مزید دکھائیں' میں دکھائے گئے ٹاپ ٹاپکس کی تعداد۔" + redirect_users_to_top_page: "نئے اور طویل غیر حاضر صارفین کو خود کار طریقے سے ٹاپ صفحے پر ریڈائرَیکٹ کریں۔" + top_page_default_timeframe: "ٹاپ وِیُو صفحے کیلئے ڈِیفالٹ ٹائم فریم۔" + show_email_on_profile: "صارف کا ای میل اُن کی پروفائل پر دکھائیں (صرف خود اُنہیں اور سٹاف کو نظر آتا ہے)" + prioritize_username_in_ux: "صارف صفحے، صارف کارڈ اور پوسٹس پر پہلے صارف نام دکھائیں (جب غیر فعال ہو تو نام پہلے دکھایا جاتا ہے)" + enable_rich_text_paste: "کمپَوزر میں ٹیکسٹ پَیسٹ کرتے وقت خود کار طریقہ سے HTML سے مارکڈائون میں تبادلہ فعال کریں۔ (تجرباتی)" + email_token_valid_hours: "پاسورڈ بھولنے / اکاؤنٹ چالو کرنے کے ٹوکن (ن) گھنٹوں کیلئے درست ہیں۔" + enable_badges: "بَیج سِسٹم فعال کریں" + enable_whispers: "ٹاپک کے اندر سٹاف کے درمیان ذاتی مواصلات کی اجازت دیں۔" + allow_index_in_robots_txt: "robots.txt فائل میں وضاحت کریں کہ یہ سائٹ وَیب سرچ اِنجنوں کی طرف سے انڈیکس کی جاسکتی ہے۔" + email_domains_blacklist: "ای میل ڈومینز کی پائپ سے علیحدہ کردہ فہرست جن کے ساتھ صارفین کو اکاؤنٹس رجسٹر کرنے کی اجازت نہیں ہے۔ مثال: mailinator.com|trashmail.net" + email_domains_whitelist: "ای میل ڈومینز کی پائپ سے علیحدہ کردہ فہرست جن کے ساتھ صارفین کو اکاؤنٹس رجسٹر کرنا لاذمی ہے۔ انتباہ: اِس فہرست کے علاوہ ای میل ڈومینز والے صارفین کو روک دیا جائے گی!" + hide_email_address_taken: "سائن اَپ کے دوران اور بھولا پاسورڈ فارم سے صارفین کو مطلع نہ کریں کہ فراہم کیے گئے ای میل ایڈریس کے ساتھ ایک اکاؤنٹ موجود ہے۔" + log_out_strict: "لاگ آؤٹ ہونے پر، صارف کیلئے تمام ڈِیوائیسِز پر تمام سیشنوں کو لاگ آؤٹ کریں" + version_checks: "ورژن اپ ڈیٹ کیلئے ڈِسکورس ہَب کو پِنگ کریں اور /admin ڈیش بورڈ پر نئے ورژن کا پیغام دکھائیں" + new_version_emails: "جب ڈِسکورس کا نیا ورژن دستیاب ہو تو contact_email ایڈریس پر ایک ای میل بھیجیں۔" + invite_expiry_days: "دنوں میں، کتنے عرصے تک صارف دعوت نامہ کلیدیں درست رہیں" + invite_passthrough_hours: "گھنٹوں میں، کتنے عرصے تک ایک صارف لاگ اِن کرنے کیلئے قبل از استعمال شدہ دعوتی کلید استعمال کرسکتا ہے" + invite_only: "عوامی رجسٹریشن غیر فعال ہے، تمام نئے صارفین کو واضح طور پر سٹاف کی طرف سے مدعو کیا جانا لاذمی ہے۔" + login_required: "اِس سائٹ پر مواد پڑھنے کیلئے اکاؤنٹ کی توثیق ہونا ضروری بنائیں، گمنام صارفین تک رسائی کو مسترد کریں۔" + min_username_length: "حروف میں صارف نام کی کم از کم لمبائی۔ انتباہ: اگر کوئی موجودہ صارفین یا گروپوں کے نام اِس سے چھوٹے ہیں، تو آپ کی سائٹ ٹوٹ جائے گی!" + max_username_length: "حروف میں صارف نام کی زیادہ سے زیادہ لمبائی۔ انتباہ: اگر کوئی موجودہ صارفین یا گروپوں کے نام اِس سے زیادہ لمبے ہیں، تو آپ کی سائٹ ٹوٹ جائے گی!" + reserved_usernames: "صارف نام جن کیلئے سائن اَپ کی اجازت نہیں ہے۔ وائلڈ کارڈ علامت * کسی بھی حرف کو صفر یا اِس سے زیادہ بار میچ کرنے کیلئے استعمال کیا جا سکتا ہے۔" + min_password_length: "پاسورڈ کی کم از کم لمبائی۔" + min_admin_password_length: "اَیڈمن کیلئے پاسورڈ کی کم از کم لمبائی۔" + password_unique_characters: "منفرد حروف کی کم از کم تعداد جو پاسورڈ میں ہونا لاذمی ہے۔" + block_common_passwords: "ایسا پاسورڈ جو 10،000 سب سے زیادہ عام پاسورڈز میںشامل ہو، اۃسے رکھنے کی اجازت نہ دیں۔" + enable_sso: "بیرونی سائٹ کے ذریعہ واحد سائن اَن کو فعال کریں (انتباہ: صارفین کے ای میل ایڈریس بیرونی سائٹ کی طرف سے توثیق کیے جانا *لازمی* ہے!)" + verbose_sso_logging: "بہت زیادہ تفصیلی SSO سے متعلقہ تشخیص /logs میں ریکارڈ کریں" + 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: "مقامی صارف نام اور پاسورڈ لاگ اِن کی بنیاد پر اکاؤنٹس فعال کریں۔ (نوٹ: دعوتیں کام کرنے کیلئے اِسے فعال ہونا لاذمی ہے)" + enable_local_logins_via_email: "صارفین کو بذریعہ ای میل بھیجے جانے والے ایک کلِک لاگ اِن لِنک کی درخواست کرنے کی اجازت دیں۔" + allow_new_registrations: "نئے صارف رجسٹریشنوں کی اجازت دیں۔ کسی کو نیا اکاؤنٹ بنانے سے روکنے کیلئے اس کو غیر چیک شدہ کریں۔" + enable_signup_cta: "واپس آنے والے گمنام صارفین کو ایک نوٹس دکھائیں جو اُنہیں اکاؤنٹ بنانے کیلۓ قائل کرے۔" + enable_yahoo_logins: "یاہُو توثیق کو فعال کریں" + enable_google_oauth2_logins: "گُوگل Oauth2 توثیق کو فعال کریں۔ گُوگل فی الحال اِس توثیق کے طریقہ کی اجازت دیتا ہے۔ کلید اور سیکرٹ کی ضرورت ہے۔" + google_oauth2_client_id: "آپ کی گُوگل ایپلی کیشن کی کلائنٹ آئی ڈی." + google_oauth2_client_secret: "آپ کی گُوگل ایپلی کیشن کا کلائنٹ سیکرٹ." + google_oauth2_prompt: "خالی جگہ سے علیحدہ کردہ سٹرنگ وَیلِیوز کی فہرست جو اِس بات کی وضاحت کرتی ہے کہ اَوتھرائزیشن سرور صارف کو دوبارہ تصدیق اور رضامندی کیلئے پوچھتا ہے کہ نہیں۔ ممکنہ اقدار کیلئے https://developers.google.com/identity/protocols/OpenIDConnect#prompt دیکھیں۔" + google_oauth2_hd: "گُوگل اَیپس ہوسٹِڈ ڈومَین جس تک کہ سائن اِن محدود ہو گا۔ مزید تفصیلات کیلئے https://developers.google.com/identity/protocols/OpenIDConnect#hd-param ملاحظہ کریں۔" + enable_twitter_logins: "ٹویٹر توثیق کو فعال کریں، twitter_consumer_key اور twitter_consumer_secret کی ضرورت ہے" + twitter_consumer_key: "ٹویٹر توثیق کیلئے کنزِیُومر کلید، https://apps.twitter.com/ پر رجسٹر کردہ" + twitter_consumer_secret: "ٹویٹر توثیق کیلئے کنزِیُومر سیکرٹ، https://apps.twitter.com/ پر رجسٹر کردہ" + enable_instagram_logins: "اِنسٹاگرام توثیق کو فعال کریں، instagram_consumer_key اور instagram_consumer_secret کی ضرورت ہے" + instagram_consumer_key: "اِنسٹاگرام توثیق کیلئے کنزِیُومر کلید" + instagram_consumer_secret: "اِنسٹاگرام توثیق کیلئے کنزِیُومر سیکرٹ" + enable_facebook_logins: "فَیس بُک توثیق کو فعال کریں، facebook_app_id اور facebook_app_secret کی ضرورت ہے" + facebook_app_id: "فَیس بُک توثیق کیلئے اَیپ آئی ڈی، https://developers.facebook.com/apps پر رجسٹر کردہ" + facebook_app_secret: "فَیس بُک توثیق کیلئے اَیپ سیکرٹ، https://developers.facebook.com/apps پر رجسٹر کردہ" + facebook_request_extra_profile_details: "فَیس بُک سے میرے بارے میں، میرا محل وقوع اور میری ویب سائٹ کی درخواست کریں۔ (آپ کی توثیق اَیپلیکیشن، فَیس بُک سے منظور شدہ ہونے کی ضرورت ہوتی ہے)" + enable_github_logins: "گِٹ ہَب توثیق کو فعال کریں، github_client_id اور github_client_secret کی ضرورت ہے" + github_client_id: "گِٹ ہَب توثیق کیلئے کلائنٹ آئی ڈی، https://github.com/settings/applications پر رجسٹر کردہ" + github_client_secret: "گِٹ ہَب توثیق کیلئے کلائنٹ سیکرٹ، https://github.com/settings/applications پر رجسٹر کردہ" + readonly_mode_during_backup: "بیک اَپ کرتے وقت صرف پڑھنے کا مَوڈ فعال کریں" + enable_backups: "ایڈمِنِسٹریٹروں کو فورم کے بیک اَپ بنانے کی اجازت دیں" + allow_restore: "رِیسٹور کی اجازت دیں، جو سائٹ کا تمام ڈیٹا تبدیل کر سکتا ہے! فالس چھوڑ دیں جب تک کہ آپ بیک اَپ بحال نہ کرنے لگیں" + maximum_backups: "ڈِسک پر رکھے جانے بیک اَپس کی زیادہ سے زیادہ تعداد۔ پرانے بیک اَپس خود کار طریقے سے حذف کر دیے جاتے ہیں" + automatic_backups_enabled: "خودکار بیک اَپس چلائیں جیسا کہ بیک اَپ فریکوئنسی میں بیان کیا گیا ہے" + backup_frequency: "بَیک اَپس کے درمیان دنوں کی تعداد۔" + enable_s3_backups: "مکمل ہونے پر S3 پر بیک اَپس اَپلوڈ کریں۔ اہم: فائل ترتیبات میں درست S3 اسناد کا درج ہونا ضروری ہے۔" + s3_backup_bucket: "بیک اَپس رکھنے کیلئے ریمَوٹ بَکِّٹ۔ انتباہ: یقینی بنائیں کہ یہ ایک زاتی بَکِّٹ ہے۔" + s3_disable_cleanup: "جب مقامی طور پر بیک اَپ ہٹا دیا جائے اُس کے ساتھ S3 پر سے بھی ہٹا دینا غیر فعال کریں۔" + backup_time_of_day: "دن کا وقت UTC جب بیک اَپ ہونا چاہئے۔" + backup_with_uploads: "شیڈول کردہ بیک اَپس میں اپلوڈز شامل کریں۔ اِس کو غیر فعال کرنے پر صرف ڈَیٹا بَیس بیک اَپ کیا جائے گا۔" + active_user_rate_limit_secs: "سیکنڈوں میں، 'last_seen_at' فیلڈ کو ہم کتنی بار اَپ ڈیٹ کریں" + verbose_localization: "UI میں توسیعی لَوکلائزَیشن کی تجاویز دکھائیں" + previous_visit_timeout_hours: "ایک وِزٹ کتنا لمبا ہونا چاہئے جس کے بعد ہم اُسے 'پچھلا' وِزٹ سمجھنا شروع کر دیں، گھنٹوں میں" + top_topics_formula_log_views_multiplier: "ٹاپ ٹاپکس کے فارمولے میں لاگ وِیوز کے مَلٹیپلائر (ن) کی عدد: `log(views_count) * (n) + op_likes_count * 0.5 + LEAST(likes_count / posts_count, 3) + 10 + log(posts_count)`" + top_topics_formula_first_post_likes_multiplier: "ٹاپ ٹاپکس کے فارمولے میں پہلی پوسٹ لائیکس کے مَلٹیپلائر (ن) کی عدد: `log(views_count) * 2 + op_likes_count * (n) + LEAST(likes_count / posts_count, 3) + 10 + log(posts_count)`" + top_topics_formula_least_likes_per_post_multiplier: "ٹاپ ٹاپکس کے فارمولے میں فی پوسٹ کم از کم لائیکس کے مَلٹیپلائر (ن) کی عدد: `log(views_count) * 2 + op_likes_count * 0.5 + LEAST(likes_count / posts_count, (n)) + 10 + log(posts_count)`" + rebake_old_posts_count: "ہر 15 منٹ پر رِیبَیک کرنے والی پرانی پوسٹس کی تعداد۔" + rate_limit_create_topic: "ایک ٹاپک بنانے کے بعد، کوئی دوسرا ٹاپک بنانے سے پہلے صارفین کو (ن) سیکنڈوں کیلئے انتظار کرنا ہوگا۔" + rate_limit_create_post: "ایک پوسٹ شائع کرنے کے بعد، دوسری پوسٹ بنانے سے پہلے صارفین کو (ن) سیکنڈوں کیلئے انتظار کرنا ہوگا۔" + rate_limit_new_user_create_topic: "ایک ٹاپک بنانے کے بعد، کوئی دوسرا ٹاپک بنانے سے پہلے نئے صارفین کو (ن) سیکنڈوں کیلئے انتظار کرنا ہوگا۔" + rate_limit_new_user_create_post: "ایک پوسٹ شائع کرنے کے بعد، دوسری پوسٹ بنانے سے پہلے نئے صارفین کو (ن) سیکنڈوں کیلئے انتظار کرنا ہوگا۔" + max_likes_per_day: "فی صارف ایک دن کے لائیکس کی زیادہ سے زیادہ تعداد۔" + max_flags_per_day: "فی صارف ایک دن کے فلَیگز کی زیادہ سے زیادہ تعداد۔" + max_bookmarks_per_day: "فی صارف ایک دن کے بُکمارکس کی زیادہ سے زیادہ تعداد۔" + max_edits_per_day: "فی صارف ایک دن کے ترامیم کی زیادہ سے زیادہ تعداد۔" + max_topics_per_day: "ایک صارف کے فی دن بنائے گئے ٹاپکس کی زیادہ سے زیادہ تعداد۔" + max_personal_messages_per_day: "صارفین کے فی دن بنائے گئے پیغامات کی زیادہ سے زیادہ تعداد۔" + max_invites_per_day: "ایک صارف کے فی دن بھیجی گئی دعوتوں کی زیادہ سے زیادہ تعداد۔" + max_topic_invitations_per_day: "ایک صارف کے فی دن بھیجی گئی ٹاپک دعوتوں کی زیادہ سے زیادہ تعداد۔" + max_logins_per_ip_per_hour: "ایک IP ایڈریس سے فی گھنٹہ لاگ اِنوں کی زیادہ سے زیادہ تعداد" + max_logins_per_ip_per_minute: "ایک IP ایڈریس سے فی منٹ لاگ اِنوں کی زیادہ سے زیادہ تعداد" + alert_admins_if_errors_per_minute: "اِیڈمن الرٹ کو متحرک کرنے کیلئے فی منٹ خرابیوں کی تعداد۔ 0 کی وَیلِیو اِس خصوصیت کو غیر فعال کردیتی ہے۔ نوٹ: رِیسٹارٹ ضروری ہے۔" + alert_admins_if_errors_per_hour: "اِیڈمن الرٹ کو متحرک کرنے کیلئے فی گھنٹہ خرابیوں کی تعداد۔ 0 کی وَیلِیو اِس خصوصیت کو غیر فعال کردیتی ہے۔ نوٹ: رِیسٹارٹ ضروری ہے۔" + categories_topics: "/categories صفحے پر دکھائے جانے والے ٹاپکس کی تعداد۔" + suggested_topics: "ٹاپک کے نچلے حصے پر دکھائے گئے تجویز کردہ ٹاپکس کی تعداد۔" + limit_suggested_to_category: "تجویز کردہ ٹاپکس میں صرف موجودہ زُمرہ سے ٹاپکس دکھائیں۔" + suggested_topics_max_days_old: "تجویز کردہ ٹاپکس ن دنوں سے زیادہ پرانے نہیں ہونے چاہئیں۔" + clean_up_uploads: "غیر قانونی ہوسٹنگ کو روکنے کیلئے یتیم غیر حوالہ دیے گئے اَپ لوڈ ہٹائیں۔ انتباہ: آپ اِس ترتیب کو فعال کرنے سے پہلے آپ شاید /uploads ڈائریکٹری کو بیک اَپ کرنا چاہیں گے۔" + clean_orphan_uploads_grace_period_hours: "یتیم اَپ لوڈ ہٹانے سے پہلے رعایتی مدت (گھنٹوں میں)۔" + purge_deleted_uploads_grace_period_days: "حذف کردہ اَپ لوڈ مٹا دینے سے پہلے رعایتی مدت (دنوں میں)۔" + purge_unactivated_users_grace_period_days: "صارف کی طرف سے اکاؤنٹ اَیکٹیوَیٹ نہ کرنے کی رعایتی مدت (دنوں میں)، جس کے بعد اکاؤنٹ حذف کر دیا جائے گا۔ غیر اَیکٹیوَیٹ کردہ صارفین کو کبھی نہ ہٹانے کیلئے 0 مقرر کریں۔" + enable_s3_uploads: "اَیمَیزَون S3 سٹوریج پر اَپ لوڈز رکھیں۔ اہم: درست S3 اسناد ضروری ہیں (دونوں ایکسَیس قی آئی ڈی اور سیکرٹ ایکسَیس قی دونوں کا ہونا ضروری ہو)۔" + s3_use_iam_profile: 'S3 بَکِّٹ تک رسائی فراہم کرنے کیلئے AWS EC2 اِنسٹَنس پرَوفائل کا استعمال کریں۔ نوٹ: اِس کو فعال کرنے کیلئے ڈِسکورس کو مناسب طریقے سے ترتیب کردہ ایک EC2 اِنسٹَنس کے اندر چلانے کی ضرورت ہوتی ہے، اور "s3 ایکسَیس کلید آئی ڈی" اور "s3 سیکرٹ ایکسَیس کلید" کی ترتیبات کی جگہ لے لیتا ہے۔' + s3_upload_bucket: "اَیمَیزَون S3 بَکِّٹ کا نام جس میں فائلیں اَپ لوڈ کی جائیں گی۔ انتباہ: لوئر کَیس، کسی قسم کے پیریڈ، یا انڈر سکور لازمی طور پر نہیں ہونے چاہئیں۔" + s3_access_key_id: "اَیمَیزَون S3 ایکسَیس کلید آئی ڈی جو تصاویر کو اَپ لوڈ کرنے کیلئے استعمال کیا جائے گی۔" + s3_secret_access_key: "اَیمَیزَون S3 سیکرٹ ایکسَیس کلید جو تصاویر کو اَپ لوڈ کرنے کیلئے استعمال کیا جائے گی۔" + s3_region: "اَیمَیزَون S3 خطے کا نام جو تصاویر کو اَپ لوڈ کرنے کیلئے استعمال کیا جائے گی۔" + s3_cdn_url: "تمام s3 اثاثوں کیلئے استعمال ہونے والا CDN URL (مثال: https://cdn.somewhere.com)۔ انتباہ: اِس ترتیب کو تبدیل کرنے کے بعد آپ کو تمام پرانی پوسٹس کو رِیبَیک کرنا ہوگا۔" + avatar_sizes: "خود کار طریقے سے تیار کردہ اوتار کے سائزوں کی فہرست۔" + external_system_avatars_enabled: "بیرونی سسٹم کی اوتار سروس کا استعمال کریں۔" + external_system_avatars_url: "بیرونی سسٹم کی اوتار سروس کا URL۔ اجازت شدہ متبادلات {username} {first_letter} {color} {size} ہیں" + default_opengraph_image_url: "ڈِیفالٹ اَوپَن٘گراف تصویر کا URL۔" + twitter_summary_large_image_url: "ٹویٹر خلاصہ کارڈ کی ڈِیفالٹ تصویر کا URL (کم از کم 280px چوڑائی، اور کم از کم 150px اونچائی کے سائز میں ہونا چاہئے)۔" + allow_all_attachments_for_group_messages: "گروپ کے پیغامات کیلئے تمام ای میل منسلکات کی اجازت دیں۔" + png_to_jpg_quality: "تبدیل شدہ JPG فائل کا معیار (1 سب سے کم معیار ہے، 99 بہترین معیار ہے، 100 غیر فعال)۔" + allow_staff_to_upload_any_file_in_pm: "سٹاف ارکان کو PM میں کوئی بھی فائل اَپ لوڈ کرنے کی اجازت دیں۔" + strip_image_metadata: "تصویر مَیٹا ڈَیٹا کو ہٹائیں۔" + enable_flash_video_onebox: "وَن باکس میں swf اور flv (اَیڈَوبِ فلَیش) لِنکس کو اَیمبَیڈ کرنے کی اجازت دیں۔ انتباہ: یہ سیکورٹی خطروں کو متعارف کرا سکتا ہے۔" + default_invitee_trust_level: "مدعو صارفین کیلئے ڈِیفالٹ ٹرسٹ لَیول (0-4)۔" + default_trust_level: "تمام نئے صارفین کیلئے ڈِیفالٹ ٹرسٹ لَیول (0-4)۔ انتباہ! اِسے تبدیل کرنے سے آپ کو سپَیم کیلئے سنگین خطرہ لاہک ہو جائے گا۔" + tl1_requires_topics_entered: "ٹرسٹ لَیول 1 پر ترقی ملنے سے قبل کتنے ٹاپکس میں ایک نئے صارف کا داخل ہونا ضروری ہے۔" + tl1_requires_read_posts: "ٹرسٹ لَیول 1 پر ترقی ملنے سے قبل کتنی پوسٹس کو ایک نئے صارف کا پڑھنا ضروری ہے۔" + tl1_requires_time_spent_mins: "ٹرسٹ لَیول 1 پر ترقی ملنے سے قبل کتنے منٹوں تک پوسٹس کا پڑھنا ایک نئے صارف کیلئے ضروری ہے۔" + tl2_requires_topics_entered: "ٹرسٹ لَیول 2 پر ترقی ملنے سے قبل کتنے ٹاپکس میں ایک صارف کا داخل ہونا ضروری ہے۔" + tl2_requires_read_posts: "ٹرسٹ لَیول 2 پر ترقی ملنے سے قبل کتنی پوسٹس کو ایک صارف کا پڑھنا ضروری ہے۔" + tl2_requires_time_spent_mins: "ٹرسٹ لَیول 2 پر ترقی ملنے سے قبل کتنے منٹوں تک پوسٹس کا پڑھنا ایک صارف کیلئے ضروری ہے۔" + tl2_requires_days_visited: "ٹرسٹ لَیول 2 پر ترقی ملنے سے قبل کتنے دنوں تک ایک صارف کا سائٹ وزٹ کرنا ضروری ہے۔" + tl2_requires_likes_received: "ٹرسٹ لَیول 2 پر ترقی ملنے سے قبل کتنے لائیکس ایک صارف کو موصول ہونا ضروری ہے۔" + tl2_requires_likes_given: "ٹرسٹ لَیول 2 پر ترقی ملنے سے قبل کتنے لائیکس ایک صارف کو کاسٹ کرنا ضروری ہے۔" + tl2_requires_topic_reply_count: "ٹرسٹ لَیول 2 پر ترقی ملنے سے قبل کتنے ٹاپکس کا ایک صارف کو جواب دینا ضروری ہے۔" + tl3_time_period: "ٹرسٹ لَیول 3 کے تقاضات کیلئے وقت کی مدت (دنوں میں)" + tl3_requires_days_visited: "ٹرسٹ لَیول 3 پر ترقی کیلئے کوالیفائی کرنے کیلئے کسی صارف کی طرف سے آخری (ٹ.ل.3 وقت کی مدت) دنوں میں سائٹ کے دوروں کی کم از کم تعداد۔ ٹ.ل.3 پر ترقی غیر فعال کرنے کیلئے ٹ.ل.3 وقت کی مدت سے زیادہ سَیٹ کریں۔ (0 یا اُس سے زیادہ)" + tl3_requires_topics_replied_to: "ٹرسٹ لَیول 3 پر ترقی کیلئے کوالیفائی کرنے کیلئے کسی صارف کی طرف سے آخری (ٹ.ل.3 وقت کی مدت) دنوں میں جواب دیے گئے ٹاپکس کی کم از کم تعداد۔ (0 یا اُس سے زیادہ)" + tl3_requires_topics_viewed: "ٹرسٹ لَیول 3 پر ترقی کیلئے کوالیفائی کرنے کیلئے آخری (ٹ.ل.3 وقت کی مدت) دنوں میں تخلیق کردہ نئے ٹاپکس میں سے کتنے فیصد ایک صارف کی طرف سے دیکھے گئے ہونے چاہئیں۔ (0 سے 100)" + tl3_requires_topics_viewed_cap: "گزشتہ (ٹ.ل.3 وقت کی مدت) دنوں میں دیکھے گئے ٹاپکس کی زیادہ سے زیادہ مطلوبہ تعداد۔" + tl3_requires_posts_read: "ٹرسٹ لَیول 3 پر ترقی کیلئے کوالیفائی کرنے کیلئے آخری (ٹ.ل.3 وقت کی مدت) دنوں میں تخلیق کردہ نئی پوسٹس میں سے کتنے فیصد ایک صارف کی طرف سے دیکھی گئی ہونی چاہئیں۔ (0 سے 100)" + tl3_requires_posts_read_cap: "گزشتہ (ٹ.ل.3 وقت کی مدت) دنوں میں دیکھے گئی پوسٹس کی زیادہ سے زیادہ مطلوبہ تعداد۔" + tl3_requires_topics_viewed_all_time: "ٹرسٹ لَیول 3 کیلئے کوالیفائی کرنے کیلئے صارف کی طرف سے کُل دیکھے گئے ٹاپکس کی کم از کم تعداد۔" + tl3_requires_posts_read_all_time: "ٹرسٹ لَیول 3 کیلئے کوالیفائی کرنے کیلئے صارف کی طرف سے کُل پڑھی گئی پوسٹس کی کم از کم تعداد۔" + tl3_requires_max_flagged: "ٹرسٹ لَیول 3 پر ترقی کیلئے کوالیفائی کرنے کیلئے آخری (ٹ.ل.3 وقت کی مدت) دنوں میں صارف کی x سے زیادہ پوسٹس x مختلف صارفین کی طرف سے فلَیگ نہیں کی گئی ہونی چاہئیں، جہاں x اِس ترتیب کی قدر ہے۔ (0 یا اُس سے زیادہ)" + tl3_promotion_min_duration: "دنوں کی کم از کم تعداد جب تک ٹرسٹ لَیول 3 پر ترقی قائم رہتی ہے اِس سے پہلے کہ ایک صارف کو ٹرسٹ لَیول 2 پر واپس تنزلی کی جا سکے۔" + tl3_requires_likes_given: "ٹرسٹ لَیول 3 پر ترقی کیلئے کوالیفائی کرنے کیلئے آخری (ٹ.ل.3 وقت کی مدت) دنوں میں دیے گئے لائیکس کی کم از کم تعداد۔" + tl3_requires_likes_received: "ٹرسٹ لَیول 3 پر ترقی کیلئے کوالیفائی کرنے کیلئے آخری (ٹ.ل.3 وقت کی مدت) دنوں میں موصول ہوئے لائیکس کی کم از کم تعداد۔" + tl3_links_no_follow: "ٹرسٹ سطح 3 صارفین کی طرف سے پوسٹ کردہ لنکس سے rel=nofollow نہ ہٹائیں۔" + trusted_users_can_edit_others: "اعلیٰ ٹرسٹ لَیول والے صارفین کو دیگر صارفین کے مواد میں ترمیم کرنے کی اجازت دیں" + min_trust_to_create_topic: "نئے ٹاپک بنانے کیلئے کم از کم مطلوبہ ٹرسٹ لَیول۔" + allow_flagging_staff: "اگر فعال ہو، تو صارف سٹاف اکاؤنٹس کی طرف سے پوسٹس کو فلَیگ کرسکتے ہیں۔" + min_trust_to_edit_wiki_post: "وِیکی کے طور پر نشان زدہ پوسٹ میں ترمیم کرنے کیلئے کم از کم مطلوبہ ٹرسٹ لَیول۔" + min_trust_to_edit_post: "پوسٹس میں ترمیم کرنے کیلئے کم از کم مطلوبہ ٹرسٹ لَیول۔" + min_trust_to_allow_self_wiki: "صارف کی طرف سے اپنی پوسٹ وِیکی بنانے کیلئے کم از کم مطلوبہ ٹرسٹ لَیول۔" + min_trust_to_send_messages: "نئے ذاتی پیغامات بنانے کیلئے کم از کم مطلوبہ ٹرسٹ لَیول۔" + min_trust_to_send_email_messages: "ای میل کے ذریعہ نئے ذاتی پیغامات (سٹَیجڈ صارفین کو) بھیجنے کیلئے کم از کم مطلوبہ ٹرسٹ لَیول۔" + min_trust_to_flag_posts: "پوسٹس فلَیگ کرنے کیلئے کم از کم مطلوبہ ٹرسٹ لَیول" + min_trust_to_post_links: "پوسٹس میں لِنکس شامل کرنے کیلئے کم از کم مطلوبہ ٹرسٹ لَیول۔" + min_trust_to_post_images: "ایک پوسٹ میں تصاویر شامل کرنے کیلئے کم از کم مطلوبہ ٹرسٹ لَیول۔" + newuser_max_links: "ایک نیا صارف ایک پوسٹ میں کتنے لِنکس شامل کر سکتا ہے۔" + newuser_max_images: "ایک نیا صارف ایک پوسٹ میں کتنے کتنی تصاویر شامل کر سکتا ہے۔" + newuser_max_attachments: "ایک نیا صارف ایک پوسٹ میں کتنی اٹیچمنٹس شامل کر سکتا ہے۔" + newuser_max_mentions_per_post: "@نام اطلاعات کی زیادہ سے زیادہ تعداد جو ایک نیا صارف ایک پوسٹ میں استعمال کر سکتا ہے۔" + newuser_max_replies_per_topic: "ایک ٹاپک میں ایک نئے صارف کی طرف سے دیے جانے والے جوابات کی زیادہ سے زیادہ تعداد جب تک کہ کوئی اُن کو جواب نہ دے۔" + max_mentions_per_post: "@نام اطلاعات کی زیادہ سے زیادہ تعداد جو کوئی ایک پوسٹ میں استعمال کر سکتا ہے۔" + max_users_notified_per_group_mention: "ایک گروپ کے ذکر پر نوٹیفکیشن موصول ہونے والے صارفین کی زیادہ سے زیادہ تعداد (اگر حد کراس ہو جائے تو کوئی اطلاع نہیں بھیجی جائے گی)" + enable_mentions: "صارفین کو دوسرے صارفین کا ذکر کرنے کی اجازت دیں۔" + create_thumbnails: "تصاویر جو پوسٹ میں فِٹ ہونے کیلئے بہت بڑی ہوں، اُن کے تھَمب نَیل اور لائیٹ باکس تصاویر تخلیق کریں۔" + email_time_window_mins: "صارفین کو اپنی پوسٹس میں ترمیم اور اُن کو حتمی شکل دینے کا موقع دینے کیلئے، کوئی بھی نوٹیفکیشن ای میل بھیجنے سے قبل (ن) منٹوں تک انتظار کریں۔" + personal_email_time_window_seconds: "صارفین کو اپنے پیغامات میں ترمیم اور اُن کو حتمی شکل دینے کا موقع دینے کیلئے، کوئی بھی ذاتی پیغام نوٹیفکیشن ای میل بھیجنے سے قبل (ن) منٹوں تک انتظار کریں۔" + email_posts_context: "نوٹیفکیشن ای میل میں سیاق و سباق کے طور پر کتنے پچھلے جوابات شامل کیے جائیں۔" + flush_timings_secs: "ہم سرور پر ٹائمنگ ڈَیٹا کتنی دفعہ فَلَش کرتے ہیں، سَیکنڈوں میں۔" + title_max_word_length: "ایک ٹاپک کے عنوان میں الفاظ کی زیادہ سے زیادہ لمبائی، حروف میں۔" + title_min_entropy: "ایک ٹاپک کے عنوان میں کم از کم مطلوبہ اَینٹراپی (منفرد حروف، مزید کیلئے غیر انگریزی حروف)۔" + body_min_entropy: "ایک پوسٹ کے متن میں کم از کم مطلوبہ اَینٹراپی (منفرد حروف، مزید کیلئے غیر انگریزی حروف)۔" + allow_uppercase_posts: "ایک ٹاپک عنوان یا پوسٹ متن میں تمام کَیپس کی اجازت دیں۔" + title_fancy_entities: "ٹاپک عنوانات میں عام ASCII حروف کو شوخ HTML اشیاء میں تبدیل کریں، بذریعہ SmartyPants http://daringfireball.net/projects/smartypants/" + min_title_similar_length: "عنوان کی کم از کم لمبائی، اِس سے پہلے کہاُس کو اُسی طرح کے دوسرے ٹاپکس کیلئے چیک کیا جائے گا۔" + min_body_similar_length: "پوسٹ متن کی کم از کم لمبائی، اِس سے پہلے کہاُس کو اُسی طرح کے دوسرے ٹاپکس کیلئے چیک کیا جائے گا۔" + desktop_category_page_style: " /categories صفحے کیلئے بصری سٹائل۔" + category_colors: "زُمرہ جات کیلئے ہیکسا ڈَیسیمل رنگوں کے اقدار کی ایک فہرست۔" + category_style: "زُمرہ بیَجوں کیلئے بصری سٹائل۔" + max_image_size_kb: "kB میں تصویر اَپ لوڈ کا زیادہ سے زیادہ سائز۔ یہ اِنجَن٘ اَیکس (client_max_body_size) / اَپَیچی یا پراکسی میں بھی ترتیب دیا جانا لازمی ہے۔" + max_attachment_size_kb: "kB میں اٹیچمنٹ فائل اَپ لوڈ کا زیادہ سے زیادہ سائز۔ یہ اِنجَن٘ اَیکس (client_max_body_size) / اَپَیچی یا پراکسی میں بھی ترتیب دیا جانا لازمی ہے۔" + authorized_extensions: "اپ لوڈ کیلئے اجازت یافتہ فائل اَیکسٹَینشَنز کی فہرست (تمام فائل اقسام کی اجازت دینے کیلئے '*' کا استعمال کریں)" + authorized_extensions_for_staff: "سٹاف صارفین کیلئے `authorized_extensions` سائٹ ترتیب میں بیان کردہ فہرست کے علاوہ اَپ لوڈ کیلئے اجازت یافتہ فائل اَیکسٹَینشَنز کی فہرست۔ (تمام فائل اقسام کی اجازت دینے کیلئے '*' کا استعمال کریں)" + theme_authorized_extensions: "تھِیم اپ لوڈز کیلئے اجازت یافتہ فائل اَیکسٹَینشَنز کی فہرست (تمام فائل اقسام کی اجازت دینے کیلئے '*' کا استعمال کریں)" + max_similar_results: "ایک نیا ٹاپک کمپوز کرتے وقت کتنے اُسی جیسے دوسرے ٹاپک اَیڈیٹر کے اوپر دکھائے جائیں۔ موازنہ عنوان اور متن پر مبنی ہے۔" + max_image_megapixels: "ایک تصویر کیلئے مَیگا پِکسل کی زیادہ سے زیادہ عدد۔" + title_prettify: "عام عنوان ٹائپینگ کی غلطیوں کو روکیں، بشمول تمام کَیپس، سب سے پہلے حرف کا لوئر کََیس ہونا، کئی! اور؟، آخر میں اضافی ۔، وغیرہ" + topic_views_heat_low: "اتنے وِیوز کے بعد، وِیوز فیلڈ ذرا سی اُجاگر کر دی جاتی ہے۔" + topic_views_heat_medium: "اتنے وِیوز کے بعد، وِیوز فیلڈ درمیانی سی اُجاگر کر دی جاتی ہے۔" + topic_views_heat_high: "اتنے وِیوز کے بعد، وِیوز فیلڈ زور کے ساتھ اُجاگر کر دی جاتی ہے۔" + cold_age_days_low: "گفتگو کے اتنے دنوں کے بعد، آخری سرگرمی کی تاریخ ذرا سی مدھم کر دی جاتی ہے۔" + cold_age_days_medium: "گفتگو کے اتنے دنوں کے بعد، آخری سرگرمی کی تاریخ درمیانی سی مدھم کر دی جاتی ہے۔" + cold_age_days_high: "گفتگو کے اتنے دنوں کے بعد، آخری سرگرمی کی تاریخ زور سے مدھم کر دی جاتی ہے۔" + history_hours_low: "اتنے گھنٹوں میں ترمیم کردہ ایک پوسٹ کا ترمیم اشارہ ذرا سا اُجاگر کر دیا جاتا ہے۔" + history_hours_medium: "اتنے گھنٹوں میں ترمیم کردہ ایک پوسٹ کا ترمیم اشارہ درمیانا سا اُجاگر کر دیا جاتا ہے۔" + history_hours_high: "اتنے گھنٹوں میں ترمیم کردہ ایک پوسٹ کا ترمیم اشارہ زور کے ساتھ اُجاگر کر دیا جاتا ہے۔" + topic_post_like_heat_low: "جب لائیکس:پوسٹ تناسب اِس تناسب سے زیادہ ہو جاتا ہے، تو پوسٹ شمار فیلڈ ذرا سی اُجاگر کر دی جاتی ہے۔" + topic_post_like_heat_medium: "جب لائیکس:پوسٹ تناسب اِس تناسب سے زیادہ ہو جاتا ہے، تو پوسٹ شمار فیلڈ درمیانی سی اُجاگر کر دی جاتی ہے۔" + topic_post_like_heat_high: "جب لائیکس:پوسٹ تناسب اِس تناسب سے زیادہ ہو جاتا ہے، تو پوسٹ شمار فیلڈ زور کے ساتھ اُجاگر کر دی جاتی ہے۔" + faq_url: "اگر آپ کے پاس ایک FAQ کسی اور جگہ پر ہَوسٹ کیا ہوا ہے جو آپ استعمال کرنا چاہتے ہیں، تو یہاں مکمل URL فراہم کریں۔" + tos_url: "اگر آپ کے پاس ایک سروس کی شرائط کی دستاویز کسی اور جگہ پر ہَوسٹ کی ہوئی ہے جو آپ استعمال کرنا چاہتے ہیں، تو یہاں مکمل URL فراہم کریں۔" + privacy_policy_url: "اگر آپ کے پاس ایک نجی معلومات کی حفاظتی پالیسی کی دستاویز کسی اور جگہ پر ہَوسٹ کی ہوئی ہے جو آپ استعمال کرنا چاہتے ہیں، تو یہاں مکمل URL فراہم کریں۔" + log_anonymizer_details: "کیا آیک صارف کو گمنام بنانے کے بعد اُس کی تفصیلات رکھی جائیں کہ نہیں۔ GDPR کے ساتھ تعمیل کرتے وقت آپ کو اِسے بند کر دینے کی ضرورت ہوگی۔" + newuser_spam_host_threshold: "ایک نیا صارف کتنی دفعہ ایک ہی ہَوسٹ کا لِنک اپنی `newuser_spam_host_threshold` پوسٹس کے اندر شائع کر سکتا ہے، اِس سے پہلے کہ وہ سپَیم سمجھا جائے۔" + white_listed_spam_host_domains: "ڈومَینز کی فہرست جو سپیم ہَوسٹ کی جانچ پڑتال سے خارج ہے۔ نئے صارفین کو اِن ڈومینز کے لِنکس کے ساتھ پوسٹ بنانے سے کبھی روکا نہیں جائے گا۔" + staff_like_weight: "سٹاف کے لائیکس کو کتنا اضافی وزن کا عنصر دیں۔" + topic_view_duration_hours: "ہر ن گھنٹوں پر فی IP/صارف ایک نیا ٹاپک وِیو شمار کریں" + user_profile_view_duration_hours: "ہر ن گھنٹوں پر فی IP/صارف ایک نیا صارف پروفائل وِیو شمار کریں" + levenshtein_distance_spammer_emails: "سپَیمر ای میل کو مَیچ کرتے وقت، فرق حروف کی تعداد جس پر بھی ایک فزِّی میچ ہو سکے گا۔" + max_new_accounts_per_registration_ip: "اگر اِس IP کی طرف سے پہلے سے ہی (ن) ٹرسٹ لَیول 0 اکاؤنٹس موجود ہیں (اور کوئی بھی سٹاف کا رکن یا ٹ.ل.2 یا اُس سے زیادہ نہیں ہے)، اُس IP سے نئے سائن اَپ کو قبول کرنا روک دیں۔" + min_ban_entries_for_roll_up: "رول اَپ بٹن پر کلک کرتے وقت اگر کم از کم (ن) اندراج موجود ہوں، تو ایک نیا ذیلی نیٹ بَین اندراج تخلیق ہو جائے گا۔" + max_age_unmatched_emails: "غیر مَیچ شدہ سکرین کردہ ای میل اندراجات (ن) دنوں کے بعد حذف کر دیں۔" + max_age_unmatched_ips: "غیر مَیچ شدہ سکرین کردہ IP اندراجات (ن) دنوں کے بعد حذف کر دیں۔" + num_flaggers_to_close_topic: "ٹاپک کو مداخلت کیلئے خود کار طریقے سے روک دینے کیلئے فلَیگ کرنے والوں کی کم از کم تعداد" + num_flags_to_close_topic: "ٹاپک کو مداخلت کیلئے خود کار طریقے سے روک دینے کیلئے فعال فلَیگز کی کم از کم تعداد" + num_hours_to_close_topic: "مداخلت کیلئے ٹاپک کو روک دینے کیلئے گھنٹوں کی تعداد۔" + auto_respond_to_flag_actions: "ایک فلَیگ کو ڈراپ کرتے وقت خودکار جوابات فعال کریں۔" + min_first_post_typing_time: "ایک صارف پہلی پوسٹ کے دوران ملی سیکنڈوں میں کم از کم کتنا وقت ٹائپ کرے، اگر حد پوری نہ ہو تو پوسٹ خود بخود منظوری کی ضرورت کی قطار میں داخل کر دی جائے گی۔ غیر فعال کرنے کیلئے 0 سَیٹ کریں (غیر تجویز کردہ)" + auto_silence_fast_typers_on_first_post: "صارفین کو خود بخود خاموش کر دیں جو min_first_post_typing_time کو پورا نہیں کرتے" + auto_silence_fast_typers_max_trust_level: "تیز رفتار ٹائپرز کو خود بخود خاموش کرنے کیلئے زیادہ سے زیادہ ٹرسٹ لَیول" + auto_silence_first_post_regex: "کَیس کے حساب سے غیر حساس رَیج اَیکس جو اگر مَیچ کر جائے تو صارف کی طرف سے پہلی پوسٹ خاموش کر دی جائے گی اور منظوری کی قطار میں بھیج دی جائے گی۔ مثال: raging|a[bc]a تمام raging یا aba یا aca پر مشتمل پوسٹس کو پہلے پر خاموش کر دیے جانے کا سبب بن جائے گا۔ صرف پہلی پوسٹ پر لاگو ہوتا ہے۔" + flags_default_topics: "فلَیگ کردہ ٹاپکس ڈیفالٹ کے طور پر اَیڈمن سیکشن میں دکھائیں" + reply_by_email_enabled: "بذریعہ ای میل، ٹاپکس کا جواب دینا فعال کریں۔" + reply_by_email_address: "جواب بذریعہ ای میل کا آنے والا ای میل ایڈریس کیلئے ٹَیمپلیٹ، مثال کے طور پر: %{reply_key}@reply.example.com یا replies+%{reply_key}@example.com" + alternative_reply_by_email_addresses: "جواب بذریعہ ای میل کے آنے والا ای میل ایڈریس کیلئے متبادل ٹیمپلیٹس، مثال: %{reply_key}@reply.example.com|replies+%{reply_key}@example.com" + incoming_email_prefer_html: "آنے والے ای میل کیلئے ٹیکسٹ کے بجائے HTML کا استعمال کریں۔" + disable_emails: "ڈِسکورس کو کسی بھی قسم کے ای میل بھیجنے سے روکیں" + strip_images_from_short_emails: "2800 بائٹس سے کم سائز والے ای میلز سے تصاویر ہٹا دیں" + short_email_length: "بائٹس میں مختصر ای میل کی لمبائی" + display_name_on_email_from: "ای میل کی \"سے\" فیلڈ پر مکمل نام دکھائیں" + unsubscribe_via_email: "صارفین کو ای میل کے موضوع یا متن میں 'غیر سَبسکرائب' کا لِنک بھیج کر ای میلز سے غیر سَبسکرائب ہو جانے کی اجازت دیں" + unsubscribe_via_email_footer: "بھیجی گئی ای میلز کے فُوٹر میں غیر سَبسکرائب بذریعہ ای میل mailto: کا لِنک منسلک کریں" + delete_email_logs_after_days: "(ن) دنوں کے بعد ای میل لاگز حذف کریں۔ غیر متعینہ مدت تک رکھنے کیلئے 0" + max_emails_per_day_per_user: "صارفین کو فی دن بھیجی گئی ای میلز کی زیادہ سے زیادہ تعداد۔ حد کو غیر فعال کرنے کیلئے 0" + enable_staged_users: "آنے والی ای میلز سے نمٹتے وقت خود کار طریقے سے سٹَیجڈ صارفین بنائیں۔" + maximum_staged_users_per_email: "آنے والی ایک ای میل سے نمٹتے وقت تشکیل کردہ سٹَیجڈ صارفین کی زیادہ سے زیادہ تعداد۔" + auto_generated_whitelist: "ای میل پتوں کی فہرست جو خود کار طریقہ سے تخلیق شدہ مواد کیلئے چیک نہیں کیے جائیں گے۔ مثال: foo@bar.com|discourse@bar.com" + block_auto_generated_emails: "آنے والی ای میلز جو خود کار طریقہ سے تخلیق شدہ کے طور پر شناخت کی گئی ہوں، اُن کو خود بخود بلاک کریں۔" + ignore_by_title: "اُن کے عنوان کی بَیس پر آنے والی ای میلز کو نظر انداز کریں۔" + mailgun_api_key: "وَیب ھُوک٘ پیغامات کی توثیق کیلئے استعمال ہونے والی مَیلگَن سیکرٹ API کلید۔" + soft_bounce_score: "عارضی بائونس ہونے پر صارف کے ساتھ بائونس سکور شامل کر دیا جائے۔" + hard_bounce_score: "مستقل بائونس ہونے پر صارف کے ساتھ بائونس سکور شامل کر دیا جائے۔" + bounce_score_threshold: "زیادہ سے زیادہ بائونس سکور اِس سے پہلے کہ ہم صارف کو ای میل کرنا روک دیں۔" + bounce_score_threshold_deactivate: "زیادہ سے زیادہ بائونس سکور اِس سے پہلے کہ ہم صارف کو غیر فعال کر دیں۔" + reset_bounce_score_after_days: "X دنوں بعد بائونس سکور خود بخود رِی سَیٹ کریں۔" + attachment_content_type_blacklist: "مطلوبہ الفاظ کی فہرست جو مواد قِسم کی بنیاد پر منسلک اٹیچمنٹس کو بلَیک لِسٹ کرنے کیلئے استعمال ہوتی ہے۔" + attachment_filename_blacklist: "مطلوبہ الفاظ کی فہرست جو فائل نام کی بنیاد پر منسلک اٹیچمنٹس کو بلَیک لِسٹ کرنے کیلئے استعمال ہوتی ہے۔" + enable_forwarded_emails: "[BETA] صارفین کو ایک ای میل فارورڈ کر کہ ٹاپک بنائے کی اجازت دیں۔" + always_show_trimmed_content: "ہمیشہ آنے والی ای میلز کا حصے دکھائیں۔ انتباہ: ای میل پتوں کو افشاں کر سکتا ہے۔" + private_email: "مزید رازداری کیلئے ای میلز میں پوسٹس یا ٹاپکس سے مواد شامل نہ کریں۔" + manual_polling_enabled: "ای میل جوابات کیلئے API کا استعمال کرتے ہوئے ای میلز پُش کریں۔" + pop3_polling_enabled: "ای میل جوابات کیلئے پَول بذریعہ POP3 کریں۔" + pop3_polling_ssl: "POP3 سرور سے کنیکٹ کرتے وقت SSL کا استعمال کریں۔ (تجویز کردہ)" + pop3_polling_openssl_verify: "TLS سرور سرٹیفکیٹ کی تصدیق کریں (ڈِیفالٹ: فعال کردہ)" + pop3_polling_period_mins: "ای میل کیلئے POP3 اکاؤنٹ چیک کرنے کے درمیان منٹوں میں مدت۔ نوٹ: رِیسٹارٹ ضروری ہے۔" + pop3_polling_port: "پَورٹ جس پر POP3 اکاؤنٹ پَول کیا جائے۔" + pop3_polling_host: "جس ہَوسٹ کیلئے پَول کیا جائے بذریعہ POP3۔" + pop3_polling_username: "ای میل کیلئے پَول کرنے کیلئے POP3 اکاؤنٹ کا صارف نام۔" + pop3_polling_password: "ای میل کیلئے پَول کرنے کیلئے POP3 اکاؤنٹ کا پاسورڈ۔" + log_mail_processing_failures: "تمام ای میل سے نمٹتے پر پیش آنء والی خرابیوں کو http://yoursitename.com/logs پر لاگ کریں" + email_in: "صارفین کو بذریعہ ای میل نئے ٹاپکس شائع کرنے کی اجازت دیں (دستی یا pop3 پَولنگ کی ضرورت ہے)۔ ہر قِسم کیلئے \"ترتیبات\" ٹَیب میں پتوں کو ترتیب دیں۔" + email_in_min_trust: "نئے ٹاپک بذریعہ ای میل شائع کرنے کیلئے کم از کم ٹرسٹ لَیول جس کی صارف کو ضرورت ہے۔" + email_prefix: "ای میلز کے موضوع میں استعمال کیا جانے والا [لیبل]۔ یہ سَیٹ کردہ نہ ہو تو 'عنوان' پر ڈِیفالٹ ہو جائے گا۔" + email_site_title: "سائٹ سے ای میلز کے \"بھیجنے والا\" کے طور پر استعمال ہونے والا سائٹ کا عنوان۔ یہ سَیٹ نہ ہو تو 'عنوان' پر ڈِیفالٹ ہو جائے گا۔ اگر آپ کے 'عنوان' میں ایسے حروف شامل ہیں جن کی ای میل \"بھیجنے والا\" سٹرِنگ میں اجازت نہیں ہے، تو اِس ترتیب کو استعمال کریں۔" + find_related_post_with_key: "جواب دی گئی پوسٹ کو تلاش کرنے کیلئے صرف 'جواب کلید' استعمال کریں۔ انتباہ: اِس کو غیر فعال کرنے سے ای میل ایڈریس پر مبنی صارف نقالی ممکن ہو جاتی ہے۔" + minimum_topics_similar: "نئے ٹاپک تشکیل کرتے وقت اُسی طرح کے دوسرے ٹاپکس پیش کیے جانے سے پہلے کتنے ٹاپکس کے موجود ہونے کی ضرورت ہے۔" + relative_date_duration: "اشاعت کے بعد دنوں کی تعداد جب پوسٹ تاریخیں مطلق (20 فروری) کی بجائے رَیلَیٹِو (7 دن) کے طور پر دکھائی جائیں گی۔" + delete_user_max_post_age: "اُن صارفین کو حذف کرنے کی اجازت نہ دیں جن کی پہلی اشاعت (x) دنوں سے زیادہ پرانی ہو۔" + delete_all_posts_max: "تمام پوسٹس حذف کریں والے بٹن سے ایک وقت میں حذف کیے جانے والی پوسٹس کی زیادہ سے زیادہ تعداد۔ اگر ایک صارف کی اِس سے زیادہ پوسٹس ہیں ، تو سبھی پوسٹس ایک ہی وقت میں حذف نہیں ہوسکتیں اور صارف کو حذف نہیں کیا جا سکتا۔" + username_change_period: "رجسٹریشن کے بعد دنوں کی زیادہ سے زیادہ تعداد جب تک کہ اکاؤنٹس اپنا صارف نام تبدیل کرسکتے ہیں (صارف نام کی تبدیلی کو غیر فعال کرنے کیلئے 0)۔" + email_editable: "رجسٹریشن کے بعد صارفین کو اپنا ای میل ایڈریس تبدیل کرنے کی اجازت دیں۔" + logout_redirect: "لاگ آؤٹ کے بعد براؤزر کو ریڈائرَیکٹ کرنے کا مقام (مثال: http://somesite.com/logout)" + allow_uploaded_avatars: "صارفین کو اپنی مرضی کی پروفائل تصاویر اَپ لوڈ کرنے کی اجازت دیں۔" + allow_animated_avatars: "صارفین کو اَینیمَیٹِڈ GIF پروفائل تصاویر استعمال کرنے کی اجازت دیں۔ انتباہ: اِس ترتیب کو تبدیل کرنے کے بعد avatars:refresh رَیک ٹاسک چلائیں۔" + allow_animated_thumbnails: "اَینیمَیٹِڈ GIFs کے اَینیمَیٹِڈ تھَمب نَیل پیدا کریں۔" + default_avatars: "URL اوتار کے، جو نئے صارفین کے لئے ڈِیفالٹ کی طور پر استعمال کیے جائیں گے جب تک کہ وہ اُن کو تبدیل نہ کر لیں۔" + automatically_download_gravatars: "اکاؤنٹ تخلیق یا ای میل کی تبدیلی پر صارفین کے لئے گَرِیوَّٹار ڈاؤن لوڈ کریں۔" + digest_topics: "ای میل خلاصہ میں شامل کرنے کیلئے مقبول ترین ٹاپکس کی زیادہ سے زیادہ تعداد۔" + digest_posts: "ای میل خلاصہ میں شامل کرنے کیلئے مقبول ترین پوسٹس کی زیادہ سے زیادہ تعداد۔" + digest_other_topics: "ای میل خلاصہ کے \"آپ کو پیروی کردہ ٹاپکس اور زُمرہ جات\" سیکشن میں دکھائے گئے ٹاپکس کی زیادہ سے زیادہ تعداد۔" + digest_min_excerpt_length: "ای میل خلاصہ میں پوسٹ اقتباس کی کم از کم لمبائی، حروف میں۔" + suppress_digest_email_after_days: "صارفین کیلئے خلاصہ ای میلز کو روک دیں جو (ن) دنوں سے زیادہ کیلئے سائٹ پر نہیں دیکھے گئے۔" + digest_suppress_categories: "خلاصہ ای میلز سے یہ زُمرہ جات دبائیں۔" + disable_digest_emails: "تمام صارفین کیلئے خلاصہ ای میلز غیر فعال کریں۔" + email_accent_bg_color: "اَیکسنٹ رنگ جو HTML ای میلز میں کچھ عناصر کے پسِ منظر کے طور پر استعمال کیا جائے گا۔ رنگ کا نام درج کریں ('red') یا ہَیکس قدر ('#FF0000')۔" + email_accent_fg_color: "رنگ ٹَیکسٹ کا جو HTML ای میلز میں ای میل کے پسِ منظر رنگ کے اوپر ظاہر ہوتا ہے۔ رنگ کا نام درج کریں ('white') یا ہَیکس قدر ('#FFFFFF')۔" + email_link_color: "HTML ای میلز میں لِنکس کا رنگ۔ رنگ کا نام درج کریں ('blue') یا ہَیکس قدر ('#0000FF')۔" + detect_custom_avatars: "چیک کیا جائے یا نہ کیا جائے کہ صارفین نے اپنی مرضی کی پروفائل تصاویر اَپ لوڈ کی ہیں کہ نہیں۔" + max_daily_gravatar_crawls: "ایک دن میں زیادہ سے زیادہ کتنی دفعہ ڈِسکورس گَرِیوَّٹار کو اپنی مرضی کے اوتار کیلئے چیک کرے گا" + public_user_custom_fields: "ایک صارف کیلئے اپنی مرضی کے فیلڈز کی وائٹ لِسٹ جو عوامی سطح پر دکھائی جا سکتی ہے۔" + staff_user_custom_fields: "ایک صارف کیلئے اپنی مرضی کے فیلڈز کی وائٹ لِسٹ جو سٹاف کو دکھائی جا سکتی ہے۔" + enable_user_directory: "براؤزِنگ کیلئے صارفین کی ایک ڈائرِکٹری فراہم کریں" + enable_group_directory: "براؤزِنگ کیلئے گروپوں کی ایک ڈائرِکٹری فراہم کریں" + group_in_subject: "ای میل کے موضوع میں %{optional_pm} کو ذاتی پیغام میں پہلے گروپ کے نام پر سَیٹ کریں، ملاحظہ کریں: https://meta.discourse.org/t/customize-subject-format-for-standard-emails/20801" + allow_anonymous_posting: "صارفین کو گمنام مَوڈ میں تبدیل ہونے کی اجازت دیں" + anonymous_posting_min_trust_level: "گمنام پوسٹنگ کو فعال کرنے کیلئے کم از کم مطلوبہ ٹرسٹ لَیول" + anonymous_account_duration_minutes: "صارف کی گمنامی کی حفاظت کیلئے ہر صارف کیلئے ہر ن منٹ بعد ایک نیا گمنام اکاؤنٹ بنائیں۔ مثال: اگر 600 پر سَیٹ کیا جائے، تو جیسے ہی آخری پوسٹ سے 600 منٹ گزر چکے ہوں گے اور صارف گمنام مَوڈ پر تبدیل ہو گا، تو ایک نیا گمنام اکاؤنٹ پیدا کر دیا جائے گا۔" + hide_user_profiles_from_public: "گمنام صارفین کیلئے صارف کارڈ، صارف پروفائلز اور صارف ڈائرِکٹری غیر فعال کریں۔" + show_inactive_accounts: "لاگ اِن ہوے صارفین کو غیر فعال اکاؤنٹس کی پروفائلز کو براؤز کرنے کی اجازت دیں۔" + hide_suspension_reasons: "صارف پروفائلز پر عوامی طور پر معطلی کی وجوہات ظاہر نہ کریں۔" + log_personal_messages_views: "دیگر صارفین/گروپوں کیلئے ایڈمن کی طرف سے ذاتی پیغام وِیوز کو لاگ کریں۔" + user_website_domains_whitelist: "اِن ڈومینز کے ساتھ صارف ویب سائٹ کی تصدیق کی جائے گی۔ پائپ کے ساتھ الگ کی گئی فہرست۔" + allow_profile_backgrounds: "صارفین کو پروفائل پسِ منظر اَپ لوڈ کرنے کی اجازت دیں۔" + sequential_replies_threshold: "ایک ہی ٹاپک میں لگاتار کتنی پوسٹس ایک صارف شائع کرے اِس سے پہلے کہ اُسے بہت زیادہ لگاتار جوابات ہونے کی یاد دہانی کرائی جائے۔" + get_a_room_threshold: "انتباہ کرا دینے سے پہلے صارف ایک ہی ٹاپک میں ایک ہی شخص کیلئے کتنی پوسٹس شائع کرے۔" + enable_mobile_theme: "موبائل ڈِیوائیسِز ایک موبائل دوستانہ تھِیم کا استعمال کرتی ہیں، مکمل سائٹ پر تبدیل ہو جانے کی صلاحیت کے ساتھ۔ اگر آپ اپنی مرضی کی سٹائل شیٹ، جو کہ مکمل طور پر رِسپَونسِو ہو، استعمال کرنا چاہتے ہیں تو اِس کو غیر فعال کریں۔" + dominating_topic_minimum_percent: "ایک ٹاپک میں ایک ہی صارف کی طرف سے کتنے فیصد پوسٹس ہونی چاہئیں، اِس سے پہلے کہ اُسے ٹاپک پر حد سے زیادہ غالب ہو جانے کی یاد دہانی کرائی جائے۔" + disable_avatar_education_message: "اوتار تبدیل کرنے کیلئے تعلیمی پیغام کو غیر فعال کریں۔" + suppress_uncategorized_badge: "ٹاپک فہرستوں میں غیر زُمرہ جات والے ٹاپکس کیلئے بَیج نہ دکھائیں۔" + permalink_normalizations: "دائمی لِنکس میچ کرنے سے پہلے درج ذیل رَیج اَیکس کا اطلاق کریں، مثال کے طور پر: /(topic.*)\\?.*/\\1 ٹاپک روٹس میں سے قُوَیری سٹرِنگ کو نکال دے گا۔ فارمیٹ regex+string ہے، میچ کردہ کو ایکسَیس کرنے کیلئے \\1 وغیرہ کا استعمال کریں" + global_notice: "تمام زائرین کو فوری،اَیمرجنسی گلوبل بَینر نوٹس دکھائیں، اِسے چھپانے کیلئے خالی جگہ میں تبدیل کریں (HTML کی اجازت ہے)۔" + disable_edit_notifications: "سِسٹم صارف کی طرف سے ترمیم اطلاعات کو غیر فعال کریں جب 'download_remote_images_to_local' فعال ہو۔" + automatically_unpin_topics: "صارفین جب ٹاپک کے آخر تک پہنچ جائیں تو خود بخود ٹاپکس پر سے پِن ہٹا دیں۔" + read_time_word_count: "متوقع پڑھنے کے وقت کا حساب لگانے کیلئے فی منٹ الفاظ کی تعداد۔" + topic_page_title_includes_category: "ٹاپک صفحہ کے عنوان میں زُمرہ کا نام شامل ہے۔" + native_app_install_banner: "بار بار آنے والوں کو ڈِسکورس مقامی ایَپ اِنسٹال کرنے کیلئے پوچھا جائے۔" + share_anonymized_statistics: "گمنام استعمال کے اعداد و شمار کا اشتراک کریں۔" + auto_handle_queued_age: "اُن ریکارڈز کو خود کار طریقہ سے ہَینڈل کریں جو اتنے دنوں کے بعد نظر ثانی کے منتظر ہیں۔ فلَیگز کو نظر انداز کیا جائے گا۔ قطار شدہ پوسٹس اور صارفین کو مسترد کردیا جائے گا۔ اِس خصوصیت کو غیر فعال کرنے کیلئے 0 پر سَیٹ کریں۔" + max_prints_per_hour_per_user: "/print صفحہ نقوش کی زیادہ سے زیادہ تعداد (غیر فعال کرنے کیلئے 0 پر سَیٹ کریں)" + full_name_required: "مکمل نام ایک صارف کے پروفائل کا لازمی فیلڈ ہے۔" + enable_names: "صارف کا مکمل نام اُن کے پروفائل، صارف کارڈ، اور ای میل پر دکھائیں۔ ہر جگہ مکمل نام چھپانے کیلئے غیر فعال کریں۔" + display_name_on_posts: "اپنے @username کے علاوہ صارف کی پوسٹس پر اُن کا مکمل نام بھی دکھائیں۔" + show_time_gap_days: "اگر دو پوسٹس اتنے دنوں بعد شائع کی گئے ہوں، تو ٹاپک میں وقت کا فرق دکھائیں۔" + short_progress_text_threshold: "اگر ٹاپک میں پوسٹس کی تعداد اِس نمبر سے اوپر چلے جاتی ہے، تو پراگرَیس بار صرف موجودہ پوسٹ نمبر دکھائے گا۔ اگر آپ پراگرَیس بار کی چوڑائی کو تبدیل کریں گے، تو آپ کو اِس قدر کو تبدیل کرنے کی ضرورت پر سکتی ہے۔" + default_code_lang: "گِٹ ہَب کَوڈ بلاکس پر لاگو ڈِیفالٹ پروگرامِنگ زبان سِنٹیکس ہائلائیٹِنگ (lang-auto، ruby، python وغیرہ)" + warn_reviving_old_topic_age: "جب کوئی ایسے ٹاپک پر جواب دینا شروع کرے جہاں پچھلا آخری جواب اتنے دنوں سے زیادہ پرانا ہو، تو ایک انتباہ ظاہر کی جائے گی۔ 0 پر سَیٹ کر کہ غیر فعال کریں۔" + autohighlight_all_code: "تمام پہلے سے فارمَیٹ کردہ کَوڈ بلاکس پر ذبردستی کَوڈ ہائلائیٹِنگ لاگو کریں، یہاں تک کہ جب زبان بھی خاص طور پر واضح نہ کی گئی ہو۔" + highlighted_languages: "سِنٹیکس ہائلائیٹِنگ کے قوانین شامل کریں۔ (انتباہ: بہت زیادہ زبانوں کو شامل کرنے سے کارکردگی پر اثر ہو سکتا ہے) ڈَیمو کیلئے ملاحظہ کریں: https://highlightjs.org/static/demo/" + embed_truncate: "اَیمبَیڈ کی گئی پوسٹس کو تراشیں۔" + embed_support_markdown: "اَیمبَیڈ کردہ پوسٹس کیلئے مارکڈائون کی اجازت دیں۔" + allowed_href_schemes: "http اور https کے علاوہ لِنکس میں اجازت دی گئی اسکیمز۔" + embed_post_limit: "اَیمبَیڈ کیے جانے والی پوسٹس کی زیادہ سے زیادہ تعداد۔" + embed_username_required: "ٹاپک کی تخلیق کیلئے صارف نام ضروری ہے۔" + notify_about_flags_after: "اگر ایسے فلَیگ موجود ہیں جن پر اتنے گھنٹوں کے بعد بھی کام نہیں کیا جا سکا، تو سٹاف کو ذاتی پیغام بھیجیں۔ غیر فعال کرنے کیلئے 0 پر سَیٹ کریں۔" + show_create_topics_notice: "اگر سائٹ میں 5 سے کم عوامی ٹاپک موجود ہیں، تو کچھ ٹاپک تخلیق کرنے کیلئے منتظمین کو ایک نوٹس دکھائیں۔" + delete_drafts_older_than_n_days: (ن) دنوں سے پرانے ڈرافٹ حذف کریں۔ + bootstrap_mode_min_users: "بُوٹسٹرَیپ مَوڈ غیر فعال کرنے کیلئے درکار صارفین کی کم از کم تعداد (غیر فعال کرنے کیلئے 0 پر سَیٹ کریں)" + prevent_anons_from_downloading_files: "گمنام صارفین کو اٹَیچمنٹس ڈاؤن لوڈ کرنے سے روکیں۔ انتباہ: ایسا کرنے سے کوئی بھی غیر تصویر سائٹ کے اثاثہ جات، جو اٹَیچمنٹ کے طور پرشائع کیے گئے ہوں، بھی کام کرنا چھوڑ دیں گے۔" + slug_generation_method: "سلَگ تخلیق کا طریقہ منتخب کریں۔ 'انکوڈ کردہ' فیصد انکوڈِنگ سٹرِنگ تخلیق کرے گ۔. 'کوئی بھی نہیں' سلَگ کو غیر فعال کر دے گا۔" + enable_emoji: "اِیمَوجی فعال کریں" + enable_emoji_shortcuts: "عام سمائلی ٹیکسٹ جیسا کہ :) :p :( اِیمَوجیوں میں تبدیل کر دیا جائے گا" + emoji_set: "آپ اپنا اِیمَوجی کیسا پسند کریں گے؟" + enforce_square_emoji: "تمام اِیمَوجیوں کو ایک مربع اَیسپَیکٹ رَیشو پر مجبور کریں۔" + approve_post_count: "ایک نئے یا بَیسِک صارف کی طرف سے پوسٹس کی تعداد جن کا منظور کیے جانا لاذمی ہے " + approve_unless_trust_level: "اِس ٹرسٹ لَیول سے نیچے کے صارفین کیلئے پوسٹس کا منظور شدہ ہونا لازمی ہے" + approve_new_topics_unless_trust_level: "اِس ٹرسٹ لَیول سے نیچے کے صارفین کیلئے نئے ٹاپکس کا منظور شدہ ہونا لازمی ہے" + notify_about_queued_posts_after: "اگر ایسی پوسٹس موجود ہیں جو اتنے گھنٹوں کے بعد بھی جائزہ لیے جانے کی منتظر ہیں، تو تمام ماڈریٹرز کو اطلاع بھیجیں۔ اِن اطلاعات کو غیر فعال کرنے کیلئے 0 پر سَیٹ کریں۔" + auto_close_messages_post_count: "خود کار طریقہ سے بند کیے جانے سے پہلے ایک پیغام میں پوسٹس کی زیادہ سے زیادہ تعداد (غیر فعال کرنے کیلئے 0)" + auto_close_topics_post_count: "خود کار طریقہ سے بند کیے جانے سے پہلے ایک ٹاپک میں پوسٹس کی زیادہ سے زیادہ تعداد (غیر فعال کرنے کیلئے 0)" + code_formatting_style: "کمپَوزر میں کَوڈ بٹن اِس کوڈ فارمَیٹِنگ سٹائل پر ڈِیفالٹ کرے گا" + max_allowed_message_recipients: "ایک پیغام میں وصول کنندگان کی زیادہ سے زیادہ تعداد۔" + watched_words_regular_expressions: "دیکھے گئے الفاظ رَیگولر اَیکسپرَیشَن ہیں۔" + default_email_digest_frequency: "صارفین کو ڈِیفالٹ کے طور پر خلاصہ ای میل کتنی بار ملتی ہیں۔" + default_include_tl0_in_digests: "ڈِیفالٹ کے طور پر خلاصہ ای میلز میں نئے صارفین کی طرف سے پوسٹس شامل کریں۔ صارفین اسے اپنی ترجیحات میں تبدیل کرسکتے ہیں۔" + default_email_personal_messages: "ڈِیفالٹ کے طور پر ایک ای میل بھیجیں جب کوئی صارف کو پیغام بھیجتا ہے۔" + default_email_direct: "ڈِیفالٹ کے طور پر ایک ای میل بھیجیں جب کوئی صارف، کا اقتباس/ کو جواب/ ذکر یا کو مدعو کرتا ہے۔" + default_email_mailing_list_mode: "ڈِیفالٹ کے طور ہر نئی پوسٹ پر ایک ای میل بھیجیں" + default_email_mailing_list_mode_frequency: "صارفین جو مَیلنگ لِسٹ مَوڈ فعال کرتے ہیں اُن کو ڈِیفالٹ کے طور پر اِس کثرت سے ای میلز ملیں گی۔" + disable_mailing_list_mode: "صارفین کو مَیلنگ لِسٹ مَوڈ فعال کرنے سے روکیں۔" + default_email_always: "ڈِیفالٹ کے طور پر صارف کو فعال ہونے پر بھی ای میل اطلاع بھیجیں۔" + default_email_previous_replies: "ڈِیفالٹ کے طور پر ای میلز میں پچھلے جواب شامل کریں۔" + default_email_in_reply_to: "ڈِیفالٹ کے طور پر ای میلز میں جس پوسٹ کا جواب دیا گیا ہو، اُس کا اقتباس شامل کریں۔" + default_other_new_topic_duration_minutes: "عالمی ڈِیفالٹ شرط جس کیلئے کوئی ٹاپک نیا سمجھا جاتا ہے۔" + default_other_auto_track_topics_after_msecs: "عالمی ڈِیفالٹ وقت جس کے بعد ٹاپک خود کار طریقہ سے ٹرَیک کر دیا جائے گا۔" + default_other_notification_level_when_replying: " گلوبل ڈیفالٹ اطلاعات کا لَیول جب صارف کسی ٹاپک پر جواب دے۔" + default_other_external_links_in_new_tab: "ڈِیفالٹ کے طور پر تمام بیرونی ویب سائٹ کے لنکس ایک نئے ٹیب میں کھولیں۔" + default_other_enable_quoting: "ڈِیفالٹ کے طور پر روشنی ڈالے گئے ٹَیکسٹ کے لئے اقتباسی جواب فعال کریں۔" + default_other_dynamic_favicon: "ڈِیفالٹ کے طور پر براؤزر آئکن پر نئے / اَپ ڈیٹ کردہ ٹاپکس کی گنتی دکھائیں۔" + default_other_disable_jump_reply: "ڈِیفالٹ کے طور پر صارف کے جواب دینے کے بعد اُن کی پوسٹ پر نہ جائیں۔" + default_other_like_notification_frequency: "ڈِیفالٹ کے طور پر صارفین کو لائیکس پر اطلاع دیں" + default_topics_automatic_unpin: "ڈِیفالٹ کے طور پر صارفین جب ٹاپک کے آخر تک پہنچ جائیں تو خود بخود ٹاپکس پر سے پِن ہٹا دیں۔" + default_categories_watching: "زُمرہ جات کی فہرست جو ڈِیفالٹ کے طور پر دیکھے گئے ہوں۔" + default_categories_tracking: "زُمرہ جات کی فہرست جو ڈِیفالٹ کے طور پر ٹرَیک کردہ ہوں۔" + default_categories_muted: "زُمرہ جات کی فہرست جو ڈِیفالٹ کے طور پر خاموش کردہ ہوں۔" + default_categories_watching_first_post: "زُمرہ جات کی فہرست جس میں ڈِیفالٹ کے طور پر ہر نئے ٹاپک میں پہلی پوسٹ دیکھی جائے گی۔" + retain_web_hook_events_period_days: "ویب ہک اِیونٹ ریکارڈ برقرار رکھنے کیلئے دنوں کی تعداد۔" + allow_user_api_keys: "صارف API کلیدیں تخلیق کرنے کی اجازت دیں" + allow_user_api_key_scopes: "صارف API کلیدوں کیلئے اجازت دی جانے والی سکَوپس کی فہرست" + max_api_keys_per_user: "فی صارف، صارف API کلیدوں کی زیادہ سے زیادہ تعداد" + min_trust_level_for_user_api_key: "API کلیدوں کی تخلیق کیلئے مطلوبہ ٹرسٹ لَیول" + allowed_user_api_auth_redirects: "صارف API کلیدوں کیلئے توثیقی ریڈائرَیکٹ کیلئے اجازت یافتہ URL" + allowed_user_api_push_urls: "صارف API پر سرور پُش کیلئے اجازت یافتہ URL" + tagging_enabled: "ٹاپکس پر ٹیگز فعال کریں؟" + min_trust_to_create_tag: "ٹَیگ بنانے کیلئے کم از کم مطلوبہ ٹرسٹ لَیول۔" + max_tags_per_topic: "ایک ٹاپک پر لگائے جانے والے ٹَیگز کی زیادہ سے زیادہ تعداد۔" + max_tag_length: "ایک ٹَیگ میں استعمال ہونے والے حروف کی زیادہ سے زیادہ تعداد۔" + max_tag_search_results: "ٹَیگز سرچ کرتے وقت، ظاہر کردہ نتائج کی زیادہ سے زیادہ تعداد۔" + show_filter_by_tag: "ٹیگ کی بنیاد پر ایک ٹاپک فہرست کو فلٹر کرنے کیلئے ڈراپ ڈاؤن دکھائیں۔" + max_tags_in_filter_list: "فلٹر ڈراپ ڈاؤن میں دکھانے گئے ٹَیگز کی زیادہ سے زیادہ تعداد۔ سب سے زیادہ استعمال شدہ ٹَیگز دکھائے جائیں گے۔" + tags_sort_alphabetically: "ٹَیگز کو حروف تہجی کی ترتیب میں دکھائیں۔ ڈِیفالٹ کے طور پر مقبولیت کے مطابق دکھائے جائیں گے۔" + tags_listed_by_group: "ٹَیگز صفحے پر ٹَیگ گروپ کی بنیاد پر ٹَیگز کی فہرست بنائیں (/tags)۔" + tag_style: "ٹَیگ بیَجوں کیلئے بصری سٹائل۔" + staff_tags: "ٹَیگز کی ایک فہرست جو صرف سٹاف ممبران لگا سکتے ہیں" + allow_staff_to_tag_pms: "سٹاف ممبران کو کسی بھی ذاتی پیغام پر ٹَیگ لگانے کی اجازت دیں" + min_trust_level_to_tag_topics: "ٹاپکس پر ٹَیگ لگانے کیلئے کم از کم مطلوبہ ٹرسٹ لَیول" + suppress_overlapping_tags_in_list: "اگر ٹَیگز ٹاپک عنوانات میں موجود مکمل الفاظ سے مَیچ کرتے ہوں، تو ٹَیگ مت دکھائیں" + remove_muted_tags_from_latest: "تازہ ترین ٹاپک فہرست میں خاموش شدہ ٹَیگز کے ساتھ ٹَیگ کردہ ٹاپکس مت دکھائیں۔" + company_short_name: "کمپنی نام (مختصر)" + company_full_name: "کمپنی نام (مکمل)" + company_domain: "کمپنی ڈَومَین" + shared_drafts_category: "ٹاپک ڈرافٹس کیلئے ایک زُمرَہ کو مقرر کر کہ مشترکہ ڈرافٹس کی صلاحیت کو فعال کریں۔" + errors: + invalid_email: "غلط ای میل ایڈریس۔" + invalid_username: "اِس صارف نام والا کوئی صارف نہیں ہے۔" + invalid_integer_min_max: "قدر کا %{min} اور %{max} کے درمیان ہونا ضروری ہے۔" + invalid_integer_min: "قدر کا %{min} یا زیادہ ہونا ضروری ہے۔" + invalid_integer_max: "قدر %{max} سے زیادہ نہیں ہوسکتی۔" + invalid_integer: "وَیلِیو کا اِنٹیجَر ہونا لازمی ہے۔" + regex_mismatch: "وَیلِیو مطلوبہ فارمَیٹ سے مَیچ نہیں کرتی۔" + must_include_latest: "سب سے اوپر والے مَینِیو میں 'تازہ ترین' ٹَیب کا شامل ہونا لازمی ہے۔" + invalid_string: "غلط وَیلِیو۔" + invalid_string_min_max: "%{min} اور %{max} حروف کے درمیان ہونا ضروری ہے۔" + invalid_string_min: "کم از کم %{min} حروف کا ہونا ضروری ہے۔" + invalid_string_max: "%{max} حروف سے زیادہ نہ ہونا ضروری ہے۔" + invalid_reply_by_email_address: "وَیلِیو میں '%{reply_key}' موجود ہونا اور اُس کا نوٹیفکیشن ای میل سے مختلف ہونا ضروری ہے۔" + invalid_alternative_reply_by_email_addresses: "تمام وَیلِیوز میں '%{reply_key}' موجود ہونا اور اُس کا نوٹیفکیشن ای میل سے مختلف ہونا ضروری ہے۔" + pop3_polling_host_is_empty: "POP3 پولِنگ فعال کرنے سے پہلے آپ کو 'pop3 پولِنگ ہوَسٹ' سَیٹ کرنا ہوگا۔" + pop3_polling_username_is_empty: "POP3 پولِنگ فعال کرنے سے پہلے آپ کو 'pop3 پولِنگ صارف نام' سَیٹ کرنا ہوگا۔" + pop3_polling_password_is_empty: "POP3 پولِنگ فعال کرنے سے پہلے آپ کو 'pop3 پولِنگ پاسورڈ' سَیٹ کرنا ہوگا۔" + pop3_polling_authentication_failed: "POP3 کی توثیق ناکام ہوگئی۔ براہ مہربانی اپنے pop3 اسناد کی تصدیق کریں۔" + reply_by_email_address_is_empty: "بذریعہ ای میل جواب فعال کرنے سے پہلے آپ کی طرف سے ایک 'جواب بذریعہ ای میل ایڈریس' سَیٹ کرنا ضروری ہے۔" + email_polling_disabled: "بذریعہ ای میل جواب فعال کرنے سے پہلے آپ کا دستی یا POP3 پولِنگ فعال کرنا ضروری ہے۔" + user_locale_not_enabled: "اِس ترتیب کو فعال کرنے سے پہلے آپ کا 'صارف مقامیت کی اجازت' فعال کرنا ضروری ہے۔" + invalid_regex: "رَیج اَیکس غلط یا اجازت یافتہ نہیں ہے۔" + email_editable_enabled: "اِس ترتیب کو فعال کرنے سے پہلے آپ کا 'قابل ترمیم بذریعہ ای میل' غیر فعال کرنا ضروری ہے۔" + enable_sso_disabled: "اِس ترتیب کو فعال کرنے سے پہلے آپ کا 'sso فعال کریں' فعال کرنا ضروری ہے۔" + staged_users_disabled: "اِس ترتیب کو فعال کرنے سے پہلے آپ کا 'سٹَیجڈ صارفین' فعال کرنا ضروری ہے۔" + reply_by_email_disabled: "اِس ترتیب کو فعال کرنے سے پہلے آپ کا 'جواب بذریعہ ای میل' فعال کرنا ضروری ہے۔" + sso_url_is_empty: "اِس ترتیب کو فعال کرنے سے پہلے آپ کا 'sso url' مقرر کرنا ضروری ہے۔" + enable_local_logins_disabled: "اِس ترتیب کو فعال کرنے سے پہلے آپ کا 'مقامی لاگ اِن' فعال کرنا ضروری ہے۔" + search: + within_post: "%{username} کی طرف سے #%{post_number}" + types: + category: 'زُمرَہ جات' + topic: 'نتائج' + user: 'صارفین' + results_page: "'%{term}' کیلئے سرچ کے نتائج" + sso: + login_error: "لاگ اِن خرابی" + not_found: "آپ کا اکاؤنٹ نہیں مل سکا۔ براہ مہربانی سائٹ کے ایڈمِنِسٹریٹر سے رابطہ کریں۔" + account_not_approved: "آپ کے اکاؤنٹ کی منظوری زیر التواء ہے۔ منظوری ملنے پر آپ کو ایک ای میل اطلاع موصول ہو گی۔" + unknown_error: "آپ کے اکاؤنٹ کے ساتھ ایک مسئلہ درپیش ہے۔ براہ مہربانی سائٹ کے ایڈمِنِسٹریٹر سے رابطہ کریں۔" + timeout_expired: "اکاؤنٹ لاگ اِن کا وقت ختم ہوگیا، براہ مہربانی دوبارہ لاگ اِن کرنے کی کوشش کریں۔" + no_email: "کوئی ای میل پتہ فراہم نہیں گیا تھا۔ براہ مہربانی سائٹ کے ایڈمِنِسٹریٹر سے رابطہ کریں۔" + email_error: "%{email} ای میل ایڈریس کے ساتھ اکاؤنٹ رجسٹر نہیں کیا جا سکا ۔ براہ مہربانی سائٹ کے ایڈمِنِسٹریٹر سے رابطہ کریں۔" + original_poster: "اصل پوسٹ کرنے والا" + most_posts: "سب سے زیادہ پوسٹس" + most_recent_poster: "سب سے حالیہ پوسٹ کرنے والا" + frequent_poster: "اکثر پوسٹ کرنے والا" + redirected_to_top_reasons: + new_user: "ہماری کمیونٹی میں خوش آمدید! یہ سب سے زیادہ مقبول ترین حالیہ ٹاپک ہیں۔" + not_seen_in_a_month: "واپسی کی خوش آمدید! ہم نے کچھ دیر سے آپ کو یہاں نہیں دیکھا۔ جب سے آپ دور رہے ہیں، یہ اُس دورانیہ کے سب سے زیادہ مقبول ترین ٹاپک ہیں۔" + merge_posts: + edit_reason: + one: "ایک پوسٹ کو %{username} کی طرف سے ضم کیا گیا تھا" + other: "%{count} پوسٹس کو %{username} کی طرف سے ضم کیا گیا تھا" + errors: + different_topics: "مختلف ٹاپکس کی پوسٹس کو ضم نہیں کیا جا سکتا۔" + different_users: "مختلف صارفین کی پوسٹس کو ضم نہیں کیا جا سکتا۔" + move_posts: + new_topic_moderator_post: + one: "پوسٹ کو ایک نئے ٹاپک پر تقسیم کر دیا گیا تھا: %{topic_link}" + other: "%{count} پوسٹس کو ایک نئے ٹاپک پر تقسیم کر دیا گیا تھا: %{topic_link}" + existing_topic_moderator_post: + one: "پوسٹ کو ایک موجودہ ٹاپک میں ضم کر دیا گیا تھا: %{topic_link}" + other: "%{count} پوسٹس کو ایک موجودہ ٹاپک میں ضم کر دیا گیا تھا: %{topic_link}" + change_owner: + post_revision_text: "%{old_user} سے %{new_user} کو مالکیت منتقل کر دی گئی" + deleted_user: "ایک حذف شدہ صارف" + topic_statuses: + archived_enabled: "یہ ٹاپک اب آرکائیو کیا ہوا ہے۔ یہ منجمد ہے اور کسی طرح سے بھی تبدیل نہیں کیا جاسکتا۔" + archived_disabled: "یہ ٹاپک اب غیر آرکائیو شدہ ہے۔ یہ اب منجمد نہیں اور تبدیل کیا جاسکتا ہے۔" + closed_enabled: "یہ ٹاپک اب بند کر دیا گیا ہے۔ نئے جوابات کی اب اجازت نہیں ہے۔" + closed_disabled: "یہ ٹاپک اب کھول دیا گیا ہے. نئے جوابات کی اجازت ہے۔" + autoclosed_message_max_posts: + one: "1 جواب کی زیادہ سے زیادہ حد تک پہنچنے کے بعد یہ پیغام خود کار طریقہ سے بند کر دیا گیا تھا۔" + other: "%{count} جوابات کی زیادہ سے زیادہ حد تک پہنچنے کے بعد یہ پیغام خود کار طریقہ سے بند کر دیا گیا تھا۔" + autoclosed_topic_max_posts: + one: "1 جواب کی زیادہ سے زیادہ حد تک پہنچنے کے بعد یہ ٹاپک خود کار طریقہ سے بند کر دیا گیا تھا۔" + other: "%{count} جوابات کی زیادہ سے زیادہ حد تک پہنچنے کے بعد یہ ٹاپک خود کار طریقہ سے بند کر دیا گیا تھا۔" + autoclosed_enabled_days: + one: "یہ ٹاپک خود کار طریقہ سے 1 دن کے بعد بند کردیا گیا تھا۔ نئے جوابات کی اب اجازت نہیں ہے۔" + other: "یہ ٹاپک خود کار طریقہ سے %{count} دنوں کے بعد بند کردیا گیا تھا۔ نئے جوابات کی اب اجازت نہیں ہے۔" + autoclosed_enabled_hours: + one: "یہ ٹاپک خود کار طریقہ سے 1 گھنٹے کے بعد بند کردیا گیا تھا۔ نئے جوابات کی اب اجازت نہیں ہے۔" + other: "یہ ٹاپک خود کار طریقہ سے %{count} گھنٹوں کے بعد بند کردیا گیا تھا۔ نئے جوابات کی اب اجازت نہیں ہے۔" + autoclosed_enabled_minutes: + one: "یہ ٹاپک خود کار طریقہ سے 1 منٹ کے بعد بند کردیا گیا تھا۔ نئے جوابات کی اب اجازت نہیں ہے۔" + other: "یہ ٹاپک خود کار طریقہ سے %{count} منٹوں کے بعد بند کردیا گیا تھا۔ نئے جوابات کی اب اجازت نہیں ہے۔" + autoclosed_enabled_lastpost_days: + one: "یہ ٹاپک آخری جواب کے 1 دن بعد خود کار طریقہ سے بند کردیا گیا تھا۔ نئے جوابات کی اب اجازت نہیں ہے۔" + other: "یہ ٹاپک آخری جواب کے %{count} دنوں بعد خود کار طریقہ سے بند کردیا گیا تھا۔ نئے جوابات کی اب اجازت نہیں ہے۔" + autoclosed_enabled_lastpost_hours: + one: "یہ ٹاپک آخری جواب کے 1 گھنٹے بعد خود کار طریقہ سے بند کردیا گیا تھا۔ نئے جوابات کی اب اجازت نہیں ہے۔" + other: "یہ ٹاپک آخری جواب کے %{count} گھنٹوں بعد خود کار طریقہ سے بند کردیا گیا تھا۔ نئے جوابات کی اب اجازت نہیں ہے۔" + autoclosed_enabled_lastpost_minutes: + one: "یہ ٹاپک آخری جواب کے 1 منٹ بعد خود کار طریقہ سے بند کردیا گیا تھا۔ نئے جوابات کی اب اجازت نہیں ہے۔" + other: "یہ ٹاپک آخری جواب کے %{count} منٹوں بعد خود کار طریقہ سے بند کردیا گیا تھا۔ نئے جوابات کی اب اجازت نہیں ہے۔" + autoclosed_disabled_days: + one: "یہ ٹاپک خود کار طریقہ سے 1 دن کے بعد کھول دیا گیا تھا۔" + other: "یہ ٹاپک خود کار طریقہ سے %{count} دنوں کے بعد کھول دیا گیا تھا۔" + autoclosed_disabled_hours: + one: "یہ ٹاپک خود کار طریقہ سے 1 گھنٹے کے بعد کھول دیا گیا تھا۔" + other: "یہ ٹاپک خود کار طریقہ سے %{count} گھنٹوں کے بعد کھول دیا گیا تھا۔" + autoclosed_disabled_minutes: + one: "یہ ٹاپک خود کار طریقہ سے 1 منٹ کے بعد کھول دیا گیا تھا۔" + other: "یہ ٹاپک خود کار طریقہ سے %{count} منٹوں کے بعد کھول دیا گیا تھا۔" + autoclosed_disabled_lastpost_days: + one: "یہ ٹاپک آخری جواب کے 1 دن بعد خود کار طریقہ سے کھول دیا گیا تھا۔" + other: "یہ ٹاپک آخری جواب کے %{count} دنوں بعد خود کار طریقہ سے کھول دیا گیا تھا۔" + autoclosed_disabled_lastpost_hours: + one: "یہ ٹاپک آخری جواب کے 1 گھنٹے بعد خود کار طریقہ سے کھول دیا گیا تھا۔" + other: "یہ ٹاپک آخری جواب کے %{count} گھنٹوں بعد خود کار طریقہ سے کھول دیا گیا تھا۔" + autoclosed_disabled_lastpost_minutes: + one: "یہ ٹاپک آخری جواب کے 1 منٹ بعد خود کار طریقہ سے کھول دیا گیا تھا۔" + other: "یہ ٹاپک آخری جواب کے %{count} منٹوں بعد خود کار طریقہ سے کھول دیا گیا تھا۔" + autoclosed_disabled: "یہ ٹاپک اب کھول دیا گیا ہے. نئے جوابات کی اجازت ہے۔" + autoclosed_disabled_lastpost: "یہ ٹاپک اب کھول دیا گیا ہے. نئے جوابات کی اجازت ہے۔" + pinned_enabled: "یہ ٹاپک اب پِن کیا یوا ہے۔ یہ اپنے زُمرہ میں سب سے اوپر ظاہر ہو گا جب تک کہ سٹاف کی طرف سے سب کیلئے یہ پِن ہٹا دی نہ جائے یا صارفین کی طرف سے انفرادی طور پر صرف اپنے آپ کیلئے۔" + pinned_disabled: "اِس ٹاپک پر سے اب پِن ہٹا دی گئی ہے۔ یہ اب اپنے زُمرہ کے سب سے اوپر ظاہر نہیں ہو گا۔" + pinned_globally_enabled: "یہ ٹاپک اب عالمی سطح پر پِن کیا یوا ہے۔ یہ اپنے زُمرہ میں اور سبھی ٹاپک فہرستوں پر سب سے اوپر ظاہر ہو گا جب تک کہ سٹاف کی طرف سے سب کیلئے یہ پِن ہٹا دی نہ جائے یا صارفین کی طرف سے انفرادی طور پر صرف اپنے آپ کیلئے۔" + pinned_globally_disabled: "اِس ٹاپک پر سے اب پِن ہٹا دی گئی ہے۔ یہ اب اپنے زُمرہ کے سب سے اوپر ظاہر نہیں ہو گا۔" + visible_enabled: "یہ ٹاپک اب فہرست شدہ ہے۔ یہ ٹاپک فہرستوں میں ظاہر ہو گا۔" + visible_disabled: "یہ ٹاپک اب غیر فہرست شد ہے۔ یہ اب کسی ٹاپک فہرستوں میں ظاہر نہیں ہو گا۔ اِس ٹاپک تک رسائی کا واحد طریقہ براہ راست لِنک کے ذریعہ ہے۔" + auto_deleted_by_timer: "ٹائمر کی طرف سے خود بخود حذف کر دیا گیا۔" + login: + not_approved: "آپ کا اکاؤنٹ ابھی تک منظور نہیں کیا گیا ہے۔ جب آپ لاگ اِن کر سکیں گے تو آپ کو بذریعہ اِی میل مطلع کر دیا جائے گا۔" + incorrect_username_email_or_password: "غلط صارف نام، ای میل یا پاسوَرڈ" + incorrect_password: "غلط پاسوَرڈ" + wait_approval: "سائن اَپ ہونے کیلئے شکریہ۔ جب آپ کا اکاؤنٹ منظور ہو جائے گا تو ہم آپ کو مطلع کر دیں گے۔" + active: "آپ کا اکاؤنٹ چالو کر دیا گیا ہے اور استعمال کیلئے تیار ہے۔" + activate_email: "

    آپ تقریباً کام مکمل کر چکے ہیں! ہم نے %{email} پر ایک ایکٹیویشن میل بھیج دی ہے۔ براہ مہربانی اپنے اکاؤنٹ کو چالو کرنے کیلئے میل میں دی گئی ہدایات پر عمل کریں۔

    اگر یہ آپ کو موصول نہ ہو، تو اپنا سپَیم فولڈر چیک کریں۔

    " + not_activated: "ابھی آپ لاگ اِن نہیں کر سکتے۔ ہم نے آپ کو ایک ایکٹیویشن اِی میل بھیجی ہے۔ براہ مہربانی، اپنے اکاؤنٹ کو چالو کرنے کیلئے ای میل میں دی گئی ہدایات پر عمل کریں۔" + not_allowed_from_ip_address: "آپ %{username} کے طور پر اِس IP ایڈریس سے لاگ اِن نہیں ہو سکتے۔" + admin_not_allowed_from_ip_address: "آپ ایڈمن کے طور پر اِس IP ایڈریس سے لاگ اِن نہیں ہو سکتے۔" + suspended: "آپ %{date} تک لاگ اِن نہیں کر سکتے ہیں۔" + suspended_with_reason: "اکاؤنٹ %{date} تک معطل کر دیا گیا: %{reason}" + errors: "%{errors}" + not_available: "دستیاب نہیں۔ اِستعمال کریں %{suggestion}؟" + something_already_taken: "کچھ غلط ہو گیا، شاید صارف نام یا ای میل پہلے یہ سے رجسٹرڈ ہے۔ پاسورڈ بھول گیا والا لنک اِستعمال کر کے دیکھیں۔" + omniauth_error: "معذرت، آپ کا اکاؤنٹ اَوتھرائز کرنے میں ایک خرابی کا سامنا کرنا پڑا۔ شاید آپ نے اَوتھرائز کرنے کی اجازت نہیں دی؟" + omniauth_error_unknown: "آپ کے لاگ اِن سے نمٹنے میں کچھ خراب ہو گیا، براہ مہربانی دوبارہ کوشش کریں۔" + authenticator_error_no_valid_email: "%{account} سے وابستہ کسی ای میل پتہ کی اجازت نہیں ہے۔ آپ کو اپنے اکاؤنٹ کو کسی مختلف ای میل ایڈریس کے ساتھ ترتیب دینے کی ضرورت ہوسکتی ہے۔" + new_registrations_disabled: "اِس وقت کسی بھی نئے اکاؤنٹ کی رجسٹریشن کی اجازت نہیں ہے۔" + password_too_long: "پاسورڈ 200 حروف تک محدود ہیں۔" + email_too_long: "آپ کا فراہم کردہ ای میل بہت طویل ہے۔ میل باکس نام 254 حروف سے زیادہ نہیں ہونے چاہئیں، اور ڈَومین نام 253 حروف سے زیادہ نہیں ہونے چاہئیں۔" + reserved_username: "اُس صارف نام کی اجازت نہیں ہے۔" + missing_user_field: "آپ نے تمام صارف فیلڈز مکمل نہیں کی ہیں" + auth_complete: "تصدیق مکمل ہوگئی ہے۔" + click_to_continue: "جاری رکھنے کیلئے یہاں کلِک کریں۔" + already_logged_in: "افوہ، لگتا ہے کہ آپ کسی اور صارف کیلئے دعوت نامے کو قبول کرنے کی کوشش کر رہے ہیں۔ اگر آپ %{current_user} نہیں ہیں تو، براہ مہربانی لاگ آُوٹ کریں اور دوبارہ کوشش کریں۔" + second_factor_title: "دو فیکٹر توثیق" + second_factor_description: "براہ مہربانی اپنی اَیپ میں سے مطلوبہ توثیقی کَوڈ درج کریں:" + invalid_second_factor_code: "غلط توثیقی کَوڈ" + user: + no_accounts_associated: "کوئی اکاؤنٹس وابستہ نہیں" + deactivated: "بہت سے '%{email}' پر باؤنسِ ہوئے ای میل کی وجہ سے غیر فعال کر دیا گیا تھا۔" + deactivated_by_staff: "سٹاف نے غیر فعال کیا" + activated_by_staff: "سٹاف نے فعال کیا" + new_user_typed_too_fast: "نئے صارف نے بہت تیزی سے ٹائپ کیا" + content_matches_auto_block_regex: "مواد خود کار طریقہ سے بلاک کردہ رَیج اَیکس سے مَیچ کرتا ہے" + username: + short: "کم از کم %{min} حروف کا ہونا لازمی ہے" + long: "%{max} حروف سے زیادہ نہ ہونا لازمی ہے" + characters: "صرف نمبر، حروف، ڈَیشِز، اور انڈر سکَور کا شامل ہونا لازمی ہے" + unique: "منفرد ہونا لازمی ہے" + blank: "موجود ہونا لازمی ہے" + must_begin_with_alphanumeric_or_underscore: "ایک حرف، ایک نمبر یا ایک انڈر سکَور کے ساتھ شروع ہونا لازمی ہے" + must_end_with_alphanumeric: "ایک حرف یا ایک نمبر کے ساتھ ختم ہونا لازمی ہے" + must_not_contain_two_special_chars_in_seq: "2 یا اُس سے زیادہ خاص حروف (.-_) ایک سلسلہ میں شامل نہ ہونا لازمی ہے" + must_not_end_with_confusing_suffix: "عجیب لاحقہ جیسا کہ .json یا .png وغیرہ کے ساتھ ختم نہ ہونا لازمی ہے" + email: + not_allowed: "اِس ای میل پرووَائیڈر سے اجازت نہیں ہے۔ براہ مہربانی ایک اور ای میل پتہ استعمال کریں۔" + blocked: "کی اجازت نہیں ہے۔" + revoked: "%{date} تک '%{email}' کو ای میل نہیں بھیجیں گے۔" + ip_address: + blocked: "آپ کے آئی پی ایڈریس سے نئی رجسٹریشنوں کی اجازت نہیں ہے۔" + max_new_accounts_per_registration_ip: "آپ کے آئی پی ایڈریس سے نئی رجسٹریشنوں کی اجازت نہیں ہے (زیادہ سے زیادہ حد تک پہنچ گئے)۔ ایک سٹاف ممبر سے رابطہ کریں۔" + website: + domain_not_allowed: "ویب سائٹ غلط ہے۔ اجازت یافتہ ڈومَینز ہیں: %{domains}" + auto_rejected: "عمر کی وجہ سے خود کار طریقہ سے رد کر دیا گیا۔ دیکھیے auto_handle_queued_age سائٹ ترتیب۔" + destroy_reasons: + unused_staged_user: "غیر استعمال شدہ سٹَیجڈ صارف" + fixed_primary_email: "سٹَیجڈ صارف کیلئے بنیادی ای میل مقرر کر دیا گیا" + same_ip_address: "دوسرے صارفین کی طرح ایک ہی IP ایڈریس (%{ip_address})" + flags_reminder: + flags_were_submitted: + one: "فلَیگز 1 گھنٹے سے پہلے جمع کیے گئے تھے۔ [براہ مہربانی ان کا جائزہ لیں](/admin/flags)۔" + other: "فلَیگز %{count} گھنٹوں سے پہلے جمع کیے گئے تھے۔ [براہ مہربانی ان کا جائزہ لیں](/admin/flags)۔" + subject_template: + one: "نمٹائے جانے کا منتظر 1 فلَیگ" + other: "نمٹائے جانے کے منتظر %{count} فلَیگز" + unsubscribe_mailer: + title: "مَیلر غیر سَبسکرائب کریں" + subject_template: "تصدیق کریں کہ اب آپ %{site_title} سے ای میل اَپ ڈیٹس حاصل نہیں کرنا چاہتے ہیں" + text_body_template: | + کسی (ممکنہ طور پر آپ؟) نے %{site_domain_name} سے اِس ایڈریس پر ای میل اَپ ڈیٹس کو مزید نہ بھیجنے کا مطالبہ کیا۔ + اگر آپ اس کی تصدیق کرنا چاہتے ہیں، تو براہ مہربانی اِس لنک پر کلِک کریں: + + %{confirm_unsubscribe_link} + + + اگر آپ ای میل اَپ ڈیٹس موصول ہونا جاری رکھنا چاہتے ہیں، تو آپ اِس ای میل کو نظر انداز کر سکتے ہیں۔ + invite_mailer: + title: "دعوتی مَیلر" + subject_template: "%{inviter_name} نے آپ کو %{site_domain_name} پر '%{topic_title}' پر دعوت دی" + text_body_template: | + %{inviter_name} نے آپ کو ایک بحث پر مدعو کیا + + > **%{topic_title}** + > + > %{topic_excerpt} + + پر + + > %{site_title}-- %{site_description} + اگر آپ دلچسپی رکھتے ہیں، تو نیچے دیے گئے لِنک پر کلِک کریں: + + %{invite_link} + custom_invite_mailer: + title: "اپنی مرضی کا دعوتی مَیلر" + subject_template: "%{inviter_name} نے آپ کو %{site_domain_name} پر '%{topic_title}' پر دعوت دی" + text_body_template: | + %{inviter_name} نے آپ کو ایک بحث پر مدعو کیا + + > **%{topic_title}** + > + > %{topic_excerpt} + + پر + + > %{site_title}-- %{site_description} + + اِس نوٹ کے ساتھ + + > %{user_custom_message} + + اگر آپ دلچسپی رکھتے ہیں، تو نیچے دیے گئے لِنک پر کلِک کریں: + + %{invite_link} + invite_forum_mailer: + title: "دعوتی فورَم مَیلر" + subject_template: "%{inviter_name} نے آپ کو %{site_domain_name} میں شمولیت کیلئے دعوت دی" + text_body_template: | + %{inviter_name} نے آپ کو شمولیت کیلئے دعوت دی + + > **%{site_title}** + > + > %{site_description} + + اگر آپ دلچسپی رکھتے ہیں، تو نیچے دیے گئے لِنک پر کلِک کریں: + + %{invite_link} + custom_invite_forum_mailer: + title: "اپنی مرضی کا دعوتی فورَم مَیلر" + subject_template: "%{inviter_name} نے آپ کو %{site_domain_name} میں شمولیت کیلئے دعوت دی" + text_body_template: | + %{inviter_name} نے آپ کو شمولیت کیلئے دعوت دی + + > **%{site_title}** + > + > %{site_description} + + اِس نوٹ کے ساتھ + + > %{user_custom_message} + + اگر آپ دلچسپی رکھتے ہیں، تو نیچے دیے گئے لِنک پر کلِک کریں: + + %{invite_link} + invite_password_instructions: + title: "دعوت پاسوَرڈ ہدایات" + subject_template: "اپنے %{site_name} اکاؤنٹ کیلئے پاسوَرڈ مقرر کریں" + text_body_template: | + %{site_name} کیلئے اپنا دعوت نامہ قبول کرنے کا شکریہ -- خوش آمدید! + + ابھی پاسوَرڈ مقرر کرنے کیلئے اِس لِنک پر کلِک کریں: + %{base_url}/u/password-reset/%{email_token} + + (اگر اوپر والے لِنک کی میعاد ختم ہو چکی ہو، تو اپنے ای میل پتہ سے لاگ اِن کرتے وقت "میں اپنا پاسوَرڈ بھول گیا" منتخب کریں۔) + download_backup_mailer: + title: "بَیک اَپ مَیلر ڈاؤن لوڈ کریں" + subject_template: "[%{email_prefix}] سائٹ بَیک اَپ ڈاؤن لوڈ" + text_body_template: | + یہ آپ کی طرف سے درخواست کردہ [سائٹ بَیک اَپ ڈاؤن لوڈ](%{backup_file_path}) ہے۔ + + ہم نے یہ ڈاؤن لوڈ لِنک سیکیورٹی وجوہات کی بناہ پر آپ کے تصدیق کردہ ای میل ایڈریس پر بھیجا ہے۔ + مصدقہ + (اگر آپ نے اِس ڈاؤن لوڈ کی درخواست *نہیں* کی، تو آپ کو سنجیدگی سے فکرمند ہونا چاہئے -- کسی کے پاس آپ کی سائٹ تک اَیڈمِن رسائی حاصل ہے۔) + no_token: | + معذرت، یہ بَیک اَپ ڈاؤن لوڈ لِنک پہلے سے ہی استعمال کیا جا چکا یا اِس کی میعاد ختم ہو چکی ہے۔ + admin_confirmation_mailer: + title: "ایڈمن کی توثیق" + subject_template: "[%{email_prefix}] نئے ایڈمن اکاؤنٹ کی توثیق کریں" + text_body_template: | + براہ مہربانی تصدیق کریں کہ آپ **%{target_username}** کو اپنے فورَم کے ایڈمِنِسٹریٹر کے طور پر شامل کرنا چاہتے ہیں۔ + + [ایڈمِنِسٹریٹر اکاؤنٹ کی تصدیق کریں](%{admin_confirm_url}) + test_mailer: + title: "مَیلر ٹیسٹ کریں" + subject_template: "[%{email_prefix}] ای میل قابل ترسیل ہونے کا ٹیسٹ" + text_body_template: | + یہ اِس سے ایک ٹیسٹ ای میل ہے + + [**%{base_url}**][0] + + ای میل کا قابل ترسیل ہونا ترسیل پیچیدہ ہے۔ یہ چند اہم چیزیں ہیں جنہیں آپ کو پہلے چیک کرنا چاہیئے: + + - اپنی سائٹ ترتیبات میں درست طریقے سے `نوٹیفکیشن ای میل` کیلئے from: ایڈریس مقرر کرنے کا *یقین* کر لیں۔ ** آپ کے بھیجے جانے والے ای میلز کے "from" ایڈریس میں درج کردہ ڈَومین وہ ڈَومین ہے جس کی بنیاد پر آپ کی ای میل کی تصدیق کی جائے گی**۔ + + - اپنے میل کلائنٹ میں ای میل کی رَا سورس دیکھنے کا طریقہ معلوم کر لیں، تاکہ آپ اہم سراگوں کیلئے ای میل ہیڈرز کی جانچ پڑتال کر سکیں۔ Gmail میں، ہر میل کے سب سے اوپر دائیں جانب ڈراپ-ڈاؤن مَینِیو میں یہ "show original" کی آپشن ہے۔ + + - **اہم:** کیا آپ کے ISP کے پاس، جن ڈومین ناموں اور IP ایڈریسوں سے آپ ای میل بھیجتے ہیں، اُن کے ساتھ منسلک کرنے کیلئے ریوَرس DNS ریکارڈ درج ہے؟ یہاں [اپنا ریوَرس PTR ریکارڈ ٹیسٹ کریں][2]۔ اگر آپ کا ISP مناسب ریورس DNS پوائنٹر ریکارڈ درج نہیں کرتا، تو اِس کا بہت کم امکان ہے کہ آپ کا کوئی بھی ای میل ارسال کیا جا سکے گا۔ + + - کیا آپ کے ڈومَین کا [SPF ریکارڈ][8] درست ہے؟ یہاں [اپنا SPF ریکارڈ چیک کریں][1]۔ نوٹ کریں کہ SPF ریکارڈ کی درست آفیشل ریکارڈ ٹائپ TXT ہے۔ + + - کیا آپ کے ڈومَین کا [DKIM ریکارڈ][3] درست ہے؟ یہ ای میل کے قابلِ ترسیل ہونے کو نمایاں طور پر بہتر بنائے گا۔ یہاں [اپنا DKIM ریکارڈ چیک کریں][7]۔ + + اگر آپ اپنا میل سرور چلاتے ہیں، تو اِس بات کا یقین کرنے کیلئے چیک کریں کہ آپ کے میل سرور کے IP [کسی بھی ای میل بلَیک لِسٹ پر موکود نہیں ہیں][4]۔ اِس بات کی بھی تصدیق کر لیں کہ یہ یقینی طور پر پوری طرح سے قوالیفائڈ ہَوسٹ نام بھیج رہا ہے جو اپنے HELO پیغام میں DNS پر ریزَولو ہوتا ہے۔ اگر ایسا نہ ہو، تو یہ آپ کا ای میل بہت سی میل سروِسوں کی طرف سے رد کیے جانے کا بائث بنے گا۔ + + - ہم آپ کو ذور کے ساتھ تجویز دیتے ہیں کہ آپ **[mail-tester.com][mt] پر ایک میل بھیجیں** اِس بات کا یقین کرنے کیلئے کہ اوپر سب صحیح طریقے سے کام کر رہا ہے۔ + + (*آسان* راستہ ہے کہ [SendGrid][sg]، [SparkPost][sp]، [Mailgun][mg] یا [Mailjet][mj]، پر ایک مفت اکاؤنٹ بنائیں جن پر سخی مفت میلنگ پلَین ہوتے ہیں اور چھوٹی کمیونٹیوں کیلئے ٹھیک ہیں۔ اگرچہ! آپ کو ابھی بھی اپنے DNS میں SPF اور DKIM ریکارڈ مقرر کرنے کی ضرورت ہوگی) + + ہمیں امید ہے کہ آپ کو ای میل کے قابلِ ترسیل ہونے کا ٹیسٹ ٹھیک سے موصول ہو گیا ہو گا! + + قسمت کامیاب کرے، + + آپ کے [ڈِسکورس](http://www.discourse.org) پر دوست + + [0]: %{base_url} + [1]: http://www.kitterman.com/spf/validate.html + [2]: http://mxtoolbox.com/ReverseLookup.aspx + [3]: http://www.dkim.org/ + [4]: http://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]: http://www.mailgun.com/ + [mj]: https://www.mailjet.com/pricing + [mt]: http://www.mail-tester.com/ + new_version_mailer: + title: "نیا ورژن مَیلر" + subject_template: "[%{email_prefix}] نیا ڈِسکورس ورژن، اَپ ڈیٹ دستیاب ہے" + text_body_template: | + ہُرے، [ڈِسکورس](http://www.discourse.org) کا ایک نیا ورژن دستیاب ہے! + + آپ کا ورژن: %{installed_version} + نیا ورژن: **%{new_version}** + + - ہمارے آسان ** [ایک-کلِک براؤزر اَپ گریڈ](%{base_url}/admin/upgrade)** کا استعمال کرتے ہوئے اَپ گریڈ کریں + + - [ریلیز نوٹس](https://meta.discourse.org/tags/release-notes) میں ملاحظہ کریں کہ نیا کیا ہے یا [رَا گِٹ ہَب تبدیلیوں کا لاگ](https://github.com/discourse/discourse/commits/master) دیکھیں + + ڈِسکورس کی خبر، بحث، اور مدد کیلئے [meta.discourse.org](https://meta.discourse.org) وِزِٹ کریں + new_version_mailer_with_notes: + title: "نوٹس کے ساتھ نیا ورژن مَیلر" + subject_template: "[%{email_prefix}] اَپ ڈیٹ دستیاب ہے" + text_body_template: | + ہُرے، [ڈِسکورس](http://www.discourse.org) کا ایک نیا ورژن دستیاب ہے! + + آپ کا ورژن: %{installed_version} + نیا ورژن: **%{new_version}** + + - ہمارے آسان ** [ایک-کلِک براؤزر اَپ گریڈ](%{base_url}/admin/upgrade)** کا استعمال کرتے ہوئے اَپ گریڈ کریں + + - [ریلیز نوٹس](https://meta.discourse.org/tags/release-notes) میں ملاحظہ کریں کہ نیا کیا ہے یا [رَا گِٹ ہَب تبدیلیوں کا لاگ](https://github.com/discourse/discourse/commits/master) دیکھیں + + ڈِسکورس کی خبر، بحث، اور مدد کیلئے [meta.discourse.org](https://meta.discourse.org) وِزِٹ کریں + + ### ریلیز نوٹس + + %{notes} + flag_reasons: + off_topic: "آپ کی پوسٹ کو **موضوع سے ہٹ کر** کے طور پر فلَیگ کیا گیا تھا: کمیونٹی سمجھتی ہے کہ یہ ٹاپک کیلئے اچھی فِٹ نہیں ہے، جیسا کہ عنوان اور پہلی پوسٹ کی طرف سے فی الحال بیان کیا گیا ہے۔" + inappropriate: "آپ کی پوسٹ کو **نامناسب** کے طور پر فلَیگ کیا گیا تھا: کمیونٹی سمجھتی ہے کہ یہ توہین آمیز، بدسلوکی، یا [ہماری کمیونٹی کے قواعد و ضوابط]() کی خلاف ورزی ہے۔" + spam: "آپ کی پوسٹ کو **سپَیم** کے طور پر فلَیگ کیا گیا تھا: کمیونٹی سمجھتی ہے کہ یہ ایک اشتہار ہے، ایسی چیز جو کہ بجائے توقع کے مطابق مفید یا موضوع سے متعلقہ ہونے، کے فطرت میں زیادہ تر پرَومَوشنل ہے۔" + notify_moderators: "آپ کی پوسٹ کو **ماڈریٹر کی توجہ کیلئے** فلَیگ کیا گیا تھا: کمیونٹی سمجھتی ہے کہ اِس پوسٹ میں کچھ ایسا ہے جس کیلئے سٹاف ممبر کی طرد سے دستی مداخلت کی ضرورت ہے۔" + flags_dispositions: + agreed: "ہمیں بتانے کیلئے شکریہ۔ ہم اتفاق کرتے ہیں کہ ایک مسئلہ موجود ہے اور ہم اِس سے نمٹ رہے ہیں۔" + agreed_and_deleted: "ہمیں بتانے کیلئے شکریہ۔ ہم اتفاق کرتے ہیں کہ ایک مسئلہ موجود ہے اور ہم نے پوسٹ ہٹا دی ہے۔" + disagreed: "ہمیں بتانے کیلئے شکریہ۔ ہم اِس سے نمٹ رہے ہیں۔" + deferred: "ہمیں بتانے کیلئے شکریہ۔ ہم اِس سے نمٹ رہے ہیں۔" + deferred_and_deleted: "ہمیں بتانے کیلئے شکریہ۔ ہم نے پوسٹ ہٹا دی ہے۔" + temporarily_closed_due_to_flags: + one: "یہ ٹاپک کمیونٹی فلَیگز کی بڑی تعداد کی وجہ سے 1 گھنٹے کیلئے عارضی طور پر بند کردیا گیا ہے۔" + other: "یہ ٹاپک کمیونٹی فلَیگز کی بڑی تعداد کی وجہ سے %{count} گھنٹوں کیلئے عارضی طور پر بند کردیا گیا ہے۔" + system_messages: + private_topic_title: "ٹاپک #%{id}" + contents_hidden: "اِس کے مواد کو دیکھنے کیلئے براہ کرم پوسٹ پر جائیں۔" + post_hidden: + title: "پوسٹ چھاپی دی گئی" + subject_template: "کمیونٹی فلَیگز کی وجہ سے پوسٹ چھاپی دی گئی" + text_body_template: | + ہیلو، + + یہ %{site_name} کی طرف سے ایک خود کار پیغام ہے تاکہ آپ کو بتایا جاسکے کہ آپ کی پوسٹ چھپا دی گئی تھی۔ + + <%{base_url}%{url}> + + %{flag_reason} + + اِس پوسٹ کو چھپانے سے پہلے ایک سے زیادہ کمیونٹی ممبران نے اِسے فلَیگ کیا تھا، لہٰذا غور کریں کہ آپ اپنی پوسٹ میں کس طرح سے ترمیم کر سکتے ہیں کہ اُن کی رائے کی عکاسی ہو سکے۔ **آپ %{edit_delay} منٹ کے بعد اپنی پوسٹ میں ترمیم کرسکتے ہیں، اور یہ خود بخود غیر چھپی ہوئی ہو جائے گی۔** + + تاہم، اگر پوسٹ ایک دوسری دفعہ کمیونٹی کی طرف سے چھپا دی جائے، تو سٹاف کی طرف سے اِس سے نمٹنے تک اِس کی پوشیدگی برقرار رہے گی۔ + + اضافی رہنمائی کیلئے، براہ کرم ہماری [کمیونٹی کی رہنما ہدایات](%{base_url}/guidelines) دیکھیے۔ + usage_tips: + text_body_template: | + نئے صارف کے طور پر شروعات کرنے پر چند فوری تجاویز کیلئے، [اِس بلاگ پوسٹ کو دیکھیں](https://blog.discourse.org/2016/12/discourse-new-user-tips-and-tricks/)۔ + + جیسے جیسے کہ آپ یہاں شرکت کریں گے، ہم آپ کو جان پائیں گے، اور نئے صارف پر عارضی پابندیاں اٹھا دی جائیں گی۔ وقت کے ساتھ ساتھ آپ [ٹرسٹ لَیول](https://meta.discourse.org/t/what-do-user-trust-levels-do/4924) حاصل کر سکیں گے، جن میں اِس کمیونٹی کو مل کر مینیج کرنے کیلئے خصوصی صلاحیتیں شامل ہوں گی۔ + welcome_user: + title: "خوش آمدید صارف" + subject_template: "%{site_name} پر خوش آمدید!" + text_body_template: | + %{site_name} میں شمولیت اختیار کرنے کیلئے شکریہ، اور خوش آمدید! + + %{new_user_tips} + + ہم ہر وقت [مہذب کمیونٹی رویہ](%{base_url}/guidelines) پر یقین رکھتے ہیں۔ + + اپنے قیام کا لطف اٹھائیں! + welcome_invite: + title: "خوش آمدید دعوت" + subject_template: "%{site_name} پر خوش آمدید!" + text_body_template: | + %{site_name} پر اپنے دعوت نامہ کو قبول کرنے کا شکریہ -- خوش آمدید! + + - ہم نے آپ کیلئے یہ نیا اکاؤنٹ **%{username}** بنایا ہے۔ [اپنی صارف پروفائل][prefs] پر جا کر اپنا نام یا پاسوَرڈ تبدیل کریں۔ + + جب آپ لاگ اِن کریں، تو براہ کرم **اپنے اصل دعوت نامے والے ای میل ایڈریس کو استعمال کریں** — ورنہ ہم بتانے کے قابل نہیں ہوں گے کہ یہ آپ ہی ہیں! + + %{new_user_tips} + + ہم ہر وقت [مہذب کمیونٹی رویہ](%{base_url}/guidelines) پر یقین رکھتے ہیں۔ + + اپنے قیام کا لطف اٹھائیں! + + [prefs]: %{user_preferences_url} + backup_succeeded: + title: "بیک اَپ کامیاب" + subject_template: "بیک اَپ کامیابی سے مکمل کر لیا گیا" + text_body_template: | + بیک اَپ کامیاب ہو گیا تھا۔ + + اپنا نیا بیک اَپ ڈاؤن لوڈ کرنے کیلئے [ایڈمن > بیک اَپ سیکشن](%{base_url}/admin/backups) ملاحظہ کریں۔ + + یہ لاگ ہے: + + ```text + %{logs} + ``` + backup_failed: + title: "بیک اَپ ناکام" + subject_template: "بیک اَپ ناکام ہو گیا" + text_body_template: | + بیک اَپ ناکام ہو گیا ہے۔ + + یہ لاگ ہے: + + ```text + %{logs} + ``` + restore_succeeded: + title: "رِیسٹور کامیاب" + subject_template: "رِیسٹور کامیابی سے مکمل ہو گیا" + text_body_template: | + رِیسٹور کامیاب ہو گیا تھا۔ + + یہ لاگ ہے: + + ```text + %{logs} + ``` + restore_failed: + title: "رِیسٹور ناکام" + subject_template: "رِیسٹور ناکام ہو گیا" + text_body_template: | + رِیسٹور ناکام ہو گیا ہے۔ + + یہ لاگ ہے: + + ```text + %{logs} + ``` + bulk_invite_succeeded: + title: "بَلک مدعو کامیاب" + subject_template: "بَلک صارف دعوت ناموں پر کامیابی سے عملدرآمد ہو گیا" + text_body_template: "آپ کے بَلک صارف دعوت نامے فائل پر عملدرآمد کر دیا گیا، %{sent} دعوتیں میل کر دی گئیں۔" + bulk_invite_failed: + title: "بَلک مدعو ناکام" + subject_template: "بَلک صارف دعوت ناموں پر خرابں کے ساتھ عملدرآمد ہو گیا" + text_body_template: | + آپ کے بَلک صارف دعوت نامے فائل پر عملدرآمد کر دیا گیا، %{sent} دعوتیں میل کر دی گئیں اور %{failed} خرابی(وں) کا سامنا کرنا پڑا۔ + + یہ لاگ ہے: + + ```text + %{logs} + ``` + csv_export_succeeded: + title: "CSV ایکسپورٹ کامیاب" + subject_template: "ڈَیٹا ایکسپورٹ مکمل" + text_body_template: | + آپ کا ڈَیٹا ایکسپورٹ کامیاب ہو گیا! :dvd: + + %{file_name} (%{file_size}) + + درجہ بالا ڈاؤن لوڈ لِنک 48 گھنٹے تک درست رہے گا۔ + + ڈَیٹا gzip آرکائیو کے طور پر کمپرَیسڈ کیا گیا ہے۔ اگر آپ کے کھولنے پر آرکائیو خود کو ایکسٹریکٹ نہیں کی لیتی، تو یہاں تجویز کردہ ٹولز استعمال کریں: http://www.gzip.org/#faq4 + csv_export_failed: + title: "CSV ایکسپورٹ ناکام" + subject_template: "ڈَیٹا ایکسپورٹ ناکام ہو گئی" + text_body_template: "ہمیں افسوس ہے، لیکن آپ کا ڈَیٹا ایکسپورٹ ناکام ہوگیا ہے۔ برائے مہربانی لاگز چیک کریں یا [ایک سٹاف ممبر سے رابطہ کریں](%{base_url}/about)۔" + email_reject_insufficient_trust_level: + title: "ای میل مسترد ناکافی ٹرسٹ لَیول" + subject_template: "[%{email_prefix}] ای میل مسئلہ -- ناکافی ٹرسٹ لَیول" + text_body_template: | + ہمیں افسوس ہے، لیکن آپ کا %{destination} پر ای میل پیغام (عنوان %{former_title}) کام نہیں کیا۔ + + اِس ای میل ایڈریس پر نئے ٹاپک شائع کرنے کیلئے آپ کے اکاؤنٹ کے پاس مطلوبہ ٹرسٹ لَیول نہیں ہے۔ اگر آپ سمجھتے ہیں کہ یہ ایک غلطی ہے، تو [ایک سٹاف ممبر سے رابطہ کریں](%{base_url}/about)۔ + email_reject_user_not_found: + title: "ای میل مسترد صارف نہیں ملا" + subject_template: "[%{email_prefix}] ای میل مسئلہ -- صارف نہیں ملا" + text_body_template: | + ہمیں افسوس ہے، لیکن آپ کا %{destination} پر ای میل پیغام (عنوان %{former_title}) کام نہیں کیا۔ + + آپ کا جواب ایک نامعلوم ای میل ایڈریس سے بھیجا گیا تھا۔ کسی دوسرے ای میل ایڈریس سے بھیجنے کی کوشش کریں، یا [ایک سٹاف ممبر سے رابطہ کریں](%{base_url}/about)۔ + email_reject_screened_email: + title: "ای میل مسترد سکرین کردہ ای میل" + subject_template: "[%{email_prefix}] ای میل مسئلہ -- بلاک کردہ ای میل" + text_body_template: | + ہمیں افسوس ہے، لیکن آپ کا %{destination} پر ای میل پیغام (عنوان %{former_title}) کام نہیں کیا۔ + + آپ کا جواب ایک بلاک شدہ ای میل ایڈریس سے بھیجا گیا تھا۔ کسی دوسرے ای میل ایڈریس سے بھیجنے کی کوشش کریں، یا [ایک سٹاف ممبر سے رابطہ کریں](%{base_url}/about)۔ + email_reject_not_allowed_email: + title: "ای میل مسترد غیر اجازت یافتہ ای میل" + subject_template: "[%{email_prefix}] ای میل مسئلہ -- بلاک کردہ ای میل" + text_body_template: | + ہمیں افسوس ہے، لیکن آپ کا %{destination} پر ای میل پیغام (عنوان %{former_title}) کام نہیں کیا۔ + + آپ کا جواب ایک بلاک شدہ ای میل ایڈریس سے بھیجا گیا تھا۔ کسی دوسرے ای میل ایڈریس سے بھیجنے کی کوشش کریں، یا [ایک سٹاف ممبر سے رابطہ کریں](%{base_url}/about)۔ + email_reject_inactive_user: + title: "ای میل مسترد غیر فعال صارف" + subject_template: "[%{email_prefix}] ای میل مسئلہ -- غیر فعال صارف" + text_body_template: | + ہمیں افسوس ہے، لیکن آپ کا %{destination} پر ای میل پیغام (عنوان %{former_title}) کام نہیں کیا۔ + + اِس ای میل ایڈریس سے منسلک آپ کا اکاؤنٹ چالو نہیں ہے۔ ای میل بھیجنے سے پہلے اپنے اکاؤنٹ کو فعال کریں۔ + email_reject_silenced_user: + title: "ای میل مسترد خاموش کردہ صارف" + subject_template: "[%{email_prefix}] ای میل مسئلہ -- خاموش کردہ صارف" + text_body_template: | + ہمیں افسوس ہے، لیکن آپ کا %{destination} پر ای میل پیغام (عنوان %{former_title}) کام نہیں کیا۔ + + اِس ای میل ایڈریس سے منسلک آپ کا اکاؤنٹ خاموش کر دیا گیا ہے۔ + email_reject_reply_user_not_matching: + title: "ای میل مسترد صارف مَیچ نہیں کرتا" + subject_template: "[%{email_prefix}] ای میل مسئلہ -- غیر متوقع ایڈریس سے جواب" + text_body_template: | + ہمیں افسوس ہے، لیکن آپ کا %{destination} پر ای میل پیغام (عنوان %{former_title}) کام نہیں کیا۔ + + آپ کا جواب ہماری طرف سے متوقع ای میل ایڈریس کے علاوہ سے بھیجا گیا تھا، لہٰذا ہمیں یقین نہیں ہے کہ یہ وہی شخص ہے۔ کسی دوسرے ای میل ایڈریس سے بھیجنے کی کوشش کریں، یا [ایک سٹاف ممبر سے رابطہ کریں](%{base_url}/about)۔ + email_reject_no_account: + title: "ای میل مسترد اکاؤنٹ نہیں" + subject_template: "[%{email_prefix}] ای میل مسئلہ -- نامعلوم اکاؤنٹ" + text_body_template: | + ہمیں افسوس ہے، لیکن آپ کا %{destination} پر ای میل پیغام (عنوان %{former_title}) کام نہیں کیا۔ + + ہم آپ کے ای میل ایڈریس سے مَیچ کردہ کسی بھی اکاؤنٹ کو تلاش نہ کر سکے۔ کسی دوسرے ای میل ایڈریس سے بھیجنے کی کوشش کریں، یا [ایک سٹاف ممبر سے رابطہ کریں](%{base_url}/about)۔ + email_reject_empty: + title: "ای میل مسترد خالی" + subject_template: "[%{email_prefix}] ای میل مسئلہ -- مواد نہیں" + text_body_template: | + ہمیں افسوس ہے، لیکن آپ کا %{destination} پر ای میل پیغام (عنوان %{former_title}) کام نہیں کیا۔ + + ہم آپ کے ای میل میں جواب کا کوئی بھی مواد تلاش نہ کر سکے۔ + + اگر آپ کو یہ موصول ہو رہا ہے اور آپ _نے_ جواب شامل کیا تھا، تو مزید سادہ فارمَیٹِنگ کے ساتھ دوبارہ کوشش کریں۔ + email_reject_parsing: + title: "ای میل مسترد پارسِنگ" + subject_template: "[%{email_prefix}] ای میل مسئلہ -- مواد نہیں سمجھا" + text_body_template: | + ہمیں افسوس ہے، لیکن آپ کا %{destination} پر ای میل پیغام (عنوان %{former_title}) کام نہیں کیا۔ + + ای میل میں ہم آپ کا جواب تلاش نہ کر سکے۔ **یقینی بنائیں کہ آپ کا جواب ای میل کے سب سے اوپر ہے** -- ہم اِن لائن جوابات پر عمل درامد نہیں کرسکتے ہیں۔ + email_reject_invalid_access: + title: "ای میل مسترد غلط رسائی" + subject_template: "[%{email_prefix}] ای میل مسئلہ -- غلط رسائی" + text_body_template: | + ہمیں افسوس ہے، لیکن آپ کا %{destination} پر ای میل پیغام (عنوان %{former_title}) کام نہیں کیا۔ + + آپ کے اکاؤنٹ کے پاس اس زُمرہ پر نئے ٹاپک شائع کرنے کی اجازت نہیں ہے۔ اگر آپ سمجھتے ہیں کہ یہ ایک غلطی ہے، تو [ایک سٹاف ممبر سے رابطہ کریں](%{base_url}/about)۔ + email_reject_strangers_not_allowed: + title: "ای میل مسترد اجنبیوں کو اجازت نہیں" + subject_template: "[%{email_prefix}] ای میل مسئلہ -- غلط رسائی" + text_body_template: | + ہمیں افسوس ہے، لیکن آپ کا %{destination} پر ای میل پیغام (عنوان %{former_title}) کام نہیں کیا۔ + + جس زُمرہ پر آپ نے یہ ای میل بھیجا ہے، وہ صرف درست اکاؤنٹ اور معلوم ای میل ایڈریس والے صارفین کے جوابات کی اجازت دیتا ہے۔ اگر آپ سمجھتے ہیں کہ یہ ایک غلطی ہے، تو [ایک سٹاف ممبر سے رابطہ کریں](%{base_url}/about)۔ + email_reject_invalid_post: + title: "ای میل مسترد غلط پوسٹ" + subject_template: "[%{email_prefix}] ای میل مسئلہ -- پوسٹ اشاعت میں خرابی" + text_body_template: | + ہمیں افسوس ہے، لیکن آپ کا %{destination} پر ای میل پیغام (عنوان %{former_title}) کام نہیں کیا۔ + + چند ممکنہ وجوہات ہیں: پیچیدہ فارمَیٹِنگ، پیغام بہت بڑا، پیغام بہت چھوٹا۔ براہ کرم دوبارہ کوشش کریں، یا اگر یہ جاری رہے تو ویب سائٹ کے ذریعہ شائع کریں۔ + email_reject_invalid_post_specified: + title: "ای میل مسترد غلط پوسٹ متعین کردہ" + subject_template: "[%{email_prefix}] ای میل مسئلہ -- پوسٹ اشاعت میں خرابی" + text_body_template: | + ہمیں افسوس ہے، لیکن آپ کا %{destination} پر ای میل پیغام (عنوان %{former_title}) کام نہیں کیا۔ + + وجہ: + + %{post_error} + + اگر آپ مسئلہ کو درست کر سکتے ہیں، تو براہ کرم دوبارہ کوشش کریں۔ + email_reject_invalid_post_action: + title: "ای میل مسترد غلط پوسٹ اَیکشن" + subject_template: "[%{email_prefix}] ای میل مسئلہ -- غلط پوسٹ اَیکشن" + text_body_template: | + ہمیں افسوس ہے، لیکن آپ کا %{destination} پر ای میل پیغام (عنوان %{former_title}) کام نہیں کیا۔ + + پوسٹ اَیکشن نامعلوم ہے۔ براہ کرم دوبارہ کوشش کریں، یا اگر یہ جاری رہے تو ویب سائٹ کے ذریعہ شائع کریں۔ + email_reject_reply_key: + title: "ای میل مسترد جواب کلید" + subject_template: "[%{email_prefix}] ای میل مسئلہ -- نامعلوم جواب کلید" + text_body_template: | + ہمیں افسوس ہے، لیکن آپ کا %{destination} پر ای میل پیغام (عنوان %{former_title}) کام نہیں کیا۔ + + ای میل میں جواب کی کلید غلط یا نامعلوم ہے، لہٰذا ہم اِس بات کا تعین نہیں کر سکتے کہ یہ ای میل کس کے جواب میں ہے۔ [ایک سٹاف ممبر سے رابطہ کریں](%{base_url}/about)۔ + email_reject_bad_destination_address: + title: "ای میل مسترد غلط منزل ایڈریس" + subject_template: "[%{email_prefix}] ای میل مسئلہ -- نامعلوم کیلئے ایڈریس" + text_body_template: | + ہمیں افسوس ہے، لیکن آپ کا %{destination} پر ای میل پیغام (عنوان %{former_title}) کام نہیں کیا۔ + + کوئی بھی منزل ای میل ایڈریس معلوم نہیں، یا ای میل میں Message-ID کے ہیڈر میں ترمیم کی گئی ہے۔ براہ کرم یقینی بنائیں کہ سٹاف کی طرف سے فراہم کردہ صحیح ای میل ایڈریس پر آپ بھیج رہے ہیں۔ + email_reject_topic_not_found: + title: "ای میل مسترد ٹاپک نہیں ملا" + subject_template: "[%{email_prefix}] ای میل مسئلہ -- ٹاپک نہیں ملا" + text_body_template: | + ہمیں افسوس ہے، لیکن آپ کا %{destination} پر ای میل پیغام (عنوان %{former_title}) کام نہیں کیا۔ + + جس ٹاپک پر آپ جواب دے رہے ہیں وہ اب موجود نہیں ہیں -- شاید یہ حذف کر دیا گیا تھا؟ اگر آپ سمجھتے ہیں کہ یہ ایک غلطی ہے، تو [ایک سٹاف ممبر سے رابطہ کریں](%{base_url}/about)۔ + email_reject_topic_closed: + title: "ای میل مسترد ٹاپک بند" + subject_template: "[%{email_prefix}] ای میل مسئلہ -- ٹاپک بند" + text_body_template: | + ہمیں افسوس ہے، لیکن آپ کا %{destination} پر ای میل پیغام (عنوان %{former_title}) کام نہیں کیا۔ + + جس ٹاپک پر آپ جواب دے رہے ہیں وہ فی الحال بند ہے اور اَب جوابات قبول نہیں کر رہا۔ اگر آپ سمجھتے ہیں کہ یہ ایک غلطی ہے، تو [ایک سٹاف ممبر سے رابطہ کریں](%{base_url}/about)۔ + email_reject_auto_generated: + title: "ای میل مسترد خود بخود تخلیق کردہ" + subject_template: "[%{email_prefix}] ای میل مسئلہ -- خود بخود تخلیق کردہ جواب" + text_body_template: | + ہمیں افسوس ہے، لیکن آپ کا %{destination} پر ای میل پیغام (عنوان %{former_title}) کام نہیں کیا۔ + + آپ کی ای میل "خود بخود تخلیق کردہ" کے طور پر نشان زد کی گئی تھی، جس کا مطلب یہ ہے کہ کسی انسان کی طرف سے ٹائپ ہونے کی بجائے اِسے کمپیوٹر کے ذریعہ خود کار طریقہ سے بنایا گیا ہے؛ ہم ایسی قِسم کی ای میلز کو قبول نہیں کرسکتے ہیں۔ اگر آپ سمجھتے ہیں کہ یہ ایک غلطی ہے، تو [ایک سٹاف ممبر سے رابطہ کریں](%{base_url}/about)۔ + email_reject_unrecognized_error: + title: "ای میل مسترد نامعلوم خرابی" + subject_template: "[%{email_prefix}] ای میل مسئلہ -- نامعلوم خرابی" + text_body_template: | + ہمیں افسوس ہے، لیکن آپ کا %{destination} پر ای میل پیغام (عنوان %{former_title}) کام نہیں کیا۔ + + آپ کی ای میل پر عمل درامد کے دوران ایک نامعلوم خرابی کا سامنا کرنا پڑا اور وہ شائع نہیں ہوئی۔ آپ کو دوبارہ کوشش کرنی چاہئے، یا [ایک سٹاف ممبر سے رابطہ کریں](%{base_url}/about)۔ + email_error_notification: + title: "ای میل مسترد اطلاع" + subject_template: "[%{email_prefix}] ای میل مسئلہ -- POP توثیق خرابی" + text_body_template: | + بدقسمتی سے، POP سرور سے میلز پولِنگ کرنے کے دوران ایک توثیقی خرابی کا سامنا کرنا پڑا۔ + + براہ مہربانی یقینی بنائیں کہ آپ نے [سائٹ ترتیبات](%{base_url}/admin/site_settings/category/email) میں POP اسناد کو ٹھیک طریقے سے ترتیب دیا ہے۔ + + اگر POP ای میل اکاؤنٹ کیلئے ویب UI موجود ہے، تو آپ کو ویب پر لاگ اِن کرنے اور اپنی ترتیبات کو وہاں چیک کرنے کی ضرورت پڑ سکتی ہے۔ + too_many_spam_flags: + title: "بہت زیادہ سپَیم فلَیگز" + subject_template: "نیا اکاؤنٹ ہَولڈ پر" + text_body_template: | + ہیلو، + + یہ %{site_name} کی طرف سے ایک خود کار پیغام ہے تاکہ آپ کو بتایا جاسکے کہ آپ کی پوسٹس عارضی طور پر چھپا دیا گیا ہے کیونکہ وہ کمیونٹی کی طرف سے فلَیگ کی گئی تھیں۔ + + احتیاطی تدابیر کے طور پر، آپ کا نیا اکاؤنٹ نئے جوابات یا ٹاپک تخلیق کرنے سے خاموش کر دیا گیا ہے جب تک کہ ایک سٹاف ممبر آپ کے اکاؤنٹ کا جائزہ لے سکے۔ ہم تکلیف کے لیے معذرت خواہ ہیں۔ + + اضافی رہنمائی کیلئے، براہ کرم ہماری [کمیونٹی کی رہنما ہدایات](%{base_url}/guidelines) دیکھیے۔ + too_many_tl3_flags: + title: "بہت زیادہ ٹ.ل.3 فلَیگز" + subject_template: "نیا اکاؤنٹ ہَولڈ پر" + text_body_template: | + ہیلو، + + یہ %{site_name} کی طرف سے ایک خود کار پیغام ہے تاکہ آپ کو بتایا جاسکے کہ بڑی تعداد میں کمیونٹی فلَیگز کی وجہ سے آپ کا اکاؤنٹ ہَولڈ پر رکھ دیا گیا ہے۔ + + احتیاطی تدابیر کے طور پر، آپ کا نیا اکاؤنٹ نئے جوابات یا ٹاپک تخلیق کرنے سے خاموش کر دیا گیا ہے جب تک کہ ایک سٹاف ممبر آپ کے اکاؤنٹ کا جائزہ لے سکے۔ ہم تکلیف کے لیے معذرت خواہ ہیں۔ + + اضافی رہنمائی کیلئے، براہ کرم ہماری [کمیونٹی کی رہنما ہدایات](%{base_url}/guidelines) دیکھیے۔ + silenced_by_staff: + title: "سٹاف نے خاموش کیا" + subject_template: "اکاؤنٹ عارضی طور پر ہَولڈ پر" + text_body_template: | + ہیلو، + + یہ %{site_name} کی طرف سے ایک خود کار پیغام ہے تاکہ آپ کو بتایا جاسکے کہ احتیاطی تدابیر کے طور پر آپ کا اکاؤنٹ عارضی طور پر ہَولڈ پر رکھ دیا گیا ہے۔ + + براہ مہربانی براؤز کرنا جاری رکھیں، لیکن آپ جواب یا ٹاپک تخلیق نہیں کر سکیں گے جب تک کوئی [سٹاف ممبر](%{base_url}/about) آپ کی سب سے حالیہ اشاعتوں کا جائزہ نہ لیلے۔ ہم تکلیف کے لیے معذرت خواہ ہیں۔ + + اضافی رہنمائی کیلئے، ہماری [کمیونٹی کی رہنما ہدایات](%{base_url}/guidelines) دیکھیے۔ + user_automatically_silenced: + title: "صارف خود بخود خاموش کردہ" + subject_template: "نیا صارف %{username} کمیونٹی فلَیگز کے ذریعہ خاموش کر دیا گیا" + text_body_template: | + یہ ایک خودکار پیغام ہے۔ + + نیا صارف [%{username}](%{user_url}) خود کار طریقہ سے خاموش کر دیا گیا تھا کیونکہ کئی صارفین نے %{username} کی پوسٹ(س) کو فلَیگ کیا تھا۔ + + براہ مہربانی [فلَیگز کا جائزہ لیں](%{base_url}/admin/flags)۔ اگر %{username} کو اشاعت سے غلط خاموش کیا گیا تھا، تو اِس صارف کیلئے [ایڈمن صفحہ](%{user_url}) پر خاموشی ختم کر دینے والے بٹن پر کلِک کریں۔ + + اس حد کو بذریعہ `silence_new_user` سائٹ ترتیب کے تبدیل کیا جاسکتا ہے۔ + 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: "اکاؤنٹ اَب ہولڈ پر نہیں ہے" + text_body_template: | + ہیلو، + + یہ %{site_name} کی طرف سے ایک خود کار پیغام ہے آپ کو مطلع کرنے کیلئے کہ سٹاف کے جائزہ لینے کے بعد آپ کا اکاؤنٹ مزید ہَولڈ پر نہیں ہے۔ + + اَب آپ دوبارہ نئے جوابات اور ٹاپک بنا سکتے ہیں۔ آپ کے صبر کا شکریہ۔ + pending_users_reminder: + title: "زیرِ اِلتواء صارفین کیلئے یاد دہانی" + subject_template: + one: "1 صارف منظوری کا منتظر ہے" + other: "%{count} صارفین منظوری کے منتظر ہیں" + text_body_template: | + اِس فورم تک رسائی حاصل کرنے سے پہلے نئے صارف سائن اَپ منظور (یا مسترد) ہونے کے منتظر ہیں۔ + [براہ مہربانی اَیڈمن سیکشن میں اُن کا جائزہ لیں](%{base_url}/admin/users/list/pending)۔ + download_remote_images_disabled: + title: "ریمَوٹ تصاویر کے ڈاؤن لوڈ غیر فعال" + subject_template: "ریمَوٹ تصاویر کے ڈاؤن لوڈ کو غیر فعال کر دیا گیا ہے" + text_body_template: "`download_remote_images_to_local` ترتیب غیر فعال کر دی گئی تھی کیونکہ` download_remote_images_threshold` پر ڈِسک میں جگہ کی حد تک پہنچ گئے تھے۔" + dashboard_problems: + title: "ڈَیش بورڈ مسائل" + subject_template: "مسائل پائے گئے ہیں" + text_body_template: | + آپ کے اَیڈمن ڈَیش بورڈ پر کچھ مسائل کی اطلاع دی جا رہی ہے۔ + + [براہ مہربانی اُن کا جائزہ لیں اور اُنہیں حل کریں](%{base_url}/admin)۔ + new_user_of_the_month: + title: "مہینہ کے سب سے زیادہ قابل قدر نئے صارف!" + subject_template: "آپ مہینہ کے سب سے زیادہ قابل قدر نئے صارف ہیں!" + text_body_template: | + مبارک ہو، آپ نے **%{month_year} کیلئے مہینہ کے سب سے زیادہ قابل قدر نئے صارف ایوارڈ** حاصل کر لیا ہے۔ :trophy: + + یہ اعزاز صرف ایک ماہ میں دو نئے صارفین کو دیا جاتا ہے، اور یہ [بَیجز صفحہ](%{url}) پر مستقل طور پر نظر آئے گا۔ + + آپ تیزی سے ہماری کمیونٹی کے قابل قدر رکن بن گئے ہیں۔ شامل ہونے کیلئے شکریہ، اور اپنا اچھا کام جاری رکھیں! + queued_posts_reminder: + title: "قطار شدہ پوسٹس یاد دہانی" + subject_template: + one: "جائزہ لیے جانے کی منتظر 1 پوسٹ" + other: "جائزہ لیے جانے کی منتظر %{count} پوسٹس" + text_body_template: | + ہیلو، + + نئے صارفین کی پوسٹس کو ماڈرَیشَن کیلئے ہَولڈ پر رکھا گیا اور فی الحال جائزہ لیے جانے کے انتظار میں ہیں۔ [اُن کو منظور یا مسترد یہاں کریں](%{base_url}/queued-posts)۔ + unsubscribe_link: | + اِن ای میلز سے غیر سَبسکرائب کرنے کیلئے، [یہاں کلِک کریں](%{unsubscribe_url})۔ + unsubscribe_link_and_mail: | + اِن ای میلز سے غیر سَبسکرائب کرنے کیلئے، [یہاں کلِک کریں](%{unsubscribe_url})۔ + unsubscribe_mailing_list: | + آپ کو یہ موصول ہو رہا ہے کیونکہ آپ نے مَیلنگ لِسٹ مَوڈ فعال کیا ہوا ہے۔ + + اِن ای میلز سے غیر سَبسکرائب کرنے کیلئے، [یہاں کلِک کریں](%{unsubscribe_url})۔ + subject_re: "جواب:" + subject_pm: "[PM]" + user_notifications: + previous_discussion: "پچھلے جوابات" + reached_limit: + one: "جان لیں کہ: ہم روزانہ کی 1 ای میل بھیجتے ہیں۔ جو ممکنی روک دی گئی ہوں، اُن کو دیکھنے کیلئے سائٹ کو چیک کریں۔ اور ہاں، مقبول ہونے کیلئے شکریہ!" + other: "جان لیں کہ: ہم روزانہ کی %{count} ای میل بھیجتے ہیں۔ جو ممکنی روک دی گئی ہوں، اُن کو دیکھنے کیلئے سائٹ کو چیک کریں۔ اور ہاں، مقبول ہونے کیلئے شکریہ!" + in_reply_to: "کے جواب میں" + unsubscribe: + title: "غیر سَبسکرائب" + description: "اِن ای میلز کو موصول کرنے میں دلچسپی نہیں ہے؟ کوئی مسئلہ نہیں! فوری طور پر غیر سَبسکرائب کرنے کیلئے ذیل میں کلِک کریں:" + reply_by_email: "[ٹاپک وِزِٹ کریں](%{base_url}%{url}) یا جواب دینے کیلئے اس ای میل کا جواب دیں۔" + reply_by_email_pm: "[پیغام وِزِٹ کریں](%{base_url}%{url}) یا جواب دینے کیلئے اس ای میل کا جواب دیں۔" + only_reply_by_email: "جواب دینے کیلئے اس ای میل کا جواب دیں۔" + visit_link_to_respond: "جواب دینے کیلئے [ٹاپک وِزِٹ کریں](%{base_url}%{url})۔" + visit_link_to_respond_pm: "جواب دینے کیلئے [پیغام وِزِٹ کریں](%{base_url}%{url})۔" + posted_by: "%{post_date} کو %{username} نے پوسٹ کیا" + invited_group_to_private_message_body: | + %{inviter_name} نے %{group_name} کو ایک پیغام پر مدعو کیا + + > **%{topic_title}** + > + > %{topic_excerpt} + + پر + + > %{site_title}-- %{site_description} + invited_to_private_message_body: | + %{inviter_name} نے آپ کو ایک پیغام پر مدعو کیا + + > **%{topic_title}** + > + > %{topic_excerpt} + + پر + + > %{site_title}-- %{site_description} + invited_to_topic_body: | + %{inviter_name} نے آپ کو ایک بحث پر مدعو کیا + + > **%{topic_title}** + > + > %{topic_excerpt} + + پر + + > %{site_title}-- %{site_description} + user_invited_to_private_message_pm_group: + title: "صارف نے گروپ ذاتی پیغام میں مدعو کیا" + subject_template: "[%{email_prefix}]%{username} نے @%{group_name} کو پیغام '%{topic_title}' میں مدعو کیا" + text_body_template: | + %{header_instructions} + + %{message} + + %{respond_instructions} + user_invited_to_private_message_pm: + title: "صارف ذاتی پیغام میں مدعو" + subject_template: "[%{email_prefix}]%{username} نے آپ کو پیغام '%{topic_title}' میں مدعو کیا" + text_body_template: | + %{header_instructions} + + %{message} + + %{respond_instructions} + user_invited_to_private_message_pm_staged: + title: "صارف ذاتی پیغام سٹَیجڈ میں مدعو" + subject_template: "[%{email_prefix}]%{username} نے آپ کو پیغام '%{topic_title}' میں مدعو کیا" + text_body_template: | + %{header_instructions} + + %{message} + + %{respond_instructions} + user_invited_to_topic: + title: "صارف ٹاپک میں مدعو" + subject_template: "[%{email_prefix}]%{username} نے آپ کو '%{topic_title}' میں مدعو کیا" + text_body_template: | + %{header_instructions} + + %{message} + + %{respond_instructions} + user_replied: + title: "صارف کی طرف سے جواب دیا گیا" + subject_template: "[%{email_prefix}]%{topic_title}" + text_body_template: | + %{header_instructions} + + %{message} + + %{context} + + %{respond_instructions} + user_replied_pm: + title: "صارف کی طرف سے جواب دیا گیا ذاتی پیغام" + subject_template: "[%{email_prefix}] [ذاتی پیغام] %{topic_title}" + text_body_template: | + %{header_instructions} + + %{message} + + %{context} + + %{respond_instructions} + user_quoted: + title: "صارف کی طرف سے اقتباس کردہ" + subject_template: "[%{email_prefix}] %{topic_title}" + text_body_template: | + %{header_instructions} + + %{message} + + %{context} + + %{respond_instructions} + user_linked: + title: "صارف کی طرف سے لِنک کردہ" + subject_template: "[%{email_prefix}]%{topic_title}" + text_body_template: | + %{header_instructions} + + %{message} + + %{context} + + %{respond_instructions} + user_mentioned: + title: "صارف ذکر کیا گیا" + subject_template: "[%{email_prefix}]%{topic_title}" + text_body_template: | + %{header_instructions} + + %{message} + + %{context} + + %{respond_instructions} + user_group_mentioned: + title: "صارف گروپ ذکر کیا گیا" + subject_template: "[%{email_prefix}]%{topic_title}" + text_body_template: | + %{header_instructions} + + %{message} + + %{context} + + %{respond_instructions} + user_posted: + title: "صارف نے شائع کیا" + subject_template: "[%{email_prefix}]%{topic_title}" + text_body_template: | + %{header_instructions} + + %{message} + + %{context} + + %{respond_instructions} + user_watching_first_post: + title: "صارف نے پہلی پوسٹ پر نظر رکھی ہوئی ہے" + subject_template: "[%{email_prefix}]%{topic_title}" + text_body_template: | + %{header_instructions} + + %{message} + + %{context} + + %{respond_instructions} + user_posted_pm: + title: "صارف نے ذاتی پیغام شائع کیا" + subject_template: "[%{email_prefix}] [ذاتی پیغام] %{topic_title}" + text_body_template: | + %{header_instructions} + + %{message} + + %{context} + + %{respond_instructions} + user_posted_pm_staged: + title: "صارف نے ذاتی پیغام سٹَیجڈ شائع کیا" + subject_template: "%{optional_re}%{topic_title}" + text_body_template: |2 + + %{message} + account_suspended: + title: "اکاؤنٹ معطل" + subject_template: "[%{email_prefix}] آپ کا اکاؤنٹ معطل کر دیا گیا ہے" + text_body_template: | + آپ کو %{suspended_till} تک فورَم سے معطل کر دیا گیا ہے۔ + + %{reason} + + %{message} + account_silenced: + title: "اکاؤنٹ خاموش" + subject_template: "[%{email_prefix}] آپ کا اکاؤنٹ خاموش کر دیا گیا ہے" + text_body_template: | + آپ کو %{silenced_till} تک فورَم سے خاموش کر دیا گیا ہے۔ + + %{reason} + + %{message} + account_exists: + title: "اکاؤنٹ پہلے سے موجود" + subject_template: "[%{email_prefix}] اکاؤنٹ پہلے سے ہی موجود ہے" + text_body_template: | + آپ نے ابھی %{site_name} پر ایک اکاؤنٹ بنانے کی کوشش کی، یا ایک اکاؤنٹ کی ای میل کو %{email} پر تبدیل کرنے کی کوشش کی۔ تاہم، %{email} کیلئے ایک اکاؤنٹ پہلے سے ہی موجود ہے۔ + + اگر آپ کو اپنا پاسوَرڈ بھول گیا، تو [ابھی دوبارہ رِی سَیٹ کریں](%{base_url}/password-reset)۔ + + اگر آپ نے %{email} کیلئے اکاؤنٹ بنانے کی کوشش نہیں کی یا اپنا ای میل ایڈریس تبدیل کرنے کی، تو فکر مت کریں - آپ اِس پیغام کو آرام سے نظر انداز کر سکتے ہیں۔ + + اگر آپ کے پاس کوئی سوال ہیں، تو [ہمارے دوستانہ سٹاف سے رابطہ کریں](%{base_url}/about)۔ + account_second_factor_disabled: + title: "دو فیکٹر توثیق غیر فعال" + subject_template: "[%{email_prefix}] دو فیکٹر توثیق غیر فعال کردی گئی" + text_body_template: | + %{site_name} پر آپ کے اکاؤنٹ کیلئے دو فیکٹر توثیق غیر فعال کردی گئی ہے۔ اب آپ صرف اپنے پاسوَرڈ کے ساتھ لاگ اِن کرسکتے ہیں۔ ایک اضافی توثیقی کوڈ کی مزید ضرورت نہیں ہے۔ + + اگر آپ نے دو فیکٹر توثیق کو غیر فعال کرنے کا انتخاب نہیں کیا، تو کسی نے آپ کے اکاؤنٹ تک غیر مجاز رسائی حاصل کر لی ہے۔ + + اگر آپ کے پاس کوئی سوال ہیں، تو [ہمارے دوستانہ سٹاف سے رابطہ کریں](%{base_url}/about)۔ + digest: + why: "آپ کے %{last_seen_at} کو آخری وِزِٹ کے بعد سے %{site_link} کا ایک مختصر خلاصہ" + since_last_visit: "آپ کے کو آخری وِزِٹ کے بعد سے" + new_topics: "نئے ٹاپک" + unread_messages: "غیر پڑھے پیغامات" + unread_notifications: "غیر پڑھی اطلاعات" + liked_received: "لائیکس موصول ہوے" + new_posts: "نئی پوسٹس" + new_users: "نئے صارفین" + popular_topics: "مقبول ٹاپک" + follow_topic: "اِس ٹاپک کی پیروی کریں" + join_the_discussion: "مزید پڑھیں " + popular_posts: "مقبول پوسٹس" + more_new: "آپ کیلئے نیا" + subject_template: "[%{email_prefix}] خلاصہ" + unsubscribe: "یہ خلاصہ %{site_link} کی طرف سے بھیجا جاتا ہے جب ہم نے کچھ دیر سے آپ کو نہیں دیکھا ہو۔ %{email_preferences_link} کو تبدیل کریں، یا غیر سَبسکرائب کرنے کیلئے %{unsubscribe_link}۔" + your_email_settings: "آپ کی ای میل ترتیبات" + click_here: "یہاں کلِک کریں" + from: "%{site_name} خلاصہ" + preheader: "آپ کے %{last_seen_at} کو آخری وِزِٹ کے بعد سے ایک مختصر خلاصہ" + forgot_password: + title: "پاسوَرڈ بھول گیا" + subject_template: "[%{email_prefix}] پاسوَرڈ ری سَیٹ" + text_body_template: | + کسی نے پر آپ کا پاسوَرڈ [%{site_name}](%{base_url}) رِی سَیٹ کرنے کیلئے کہا۔ + + اگر یہ آپ نہیں تھے، تو آپ اِس ای میل کو آرام سے نظر انداز کر سکتے ہیں۔ + + نیا پاسوَرڈ منتخب کرنے کیلئے مندرجہ ذیل لِنک پر کلِک کریں: + %{base_url}/u/password-reset/%{email_token} + email_login: + title: "بذریعہ لِنک لاگ اِن کریں" + subject_template: "[%{email_prefix}] بذریعہ لِنک لاگ اِن کریں" + text_body_template: | + [%{site_name}](%{base_url}) پر لاگ اِن کرنے کیلئے آپ کا لِنک یہ ہے۔ + + اگر آپ نے اِس لِنک کی درخواست نہیں کی، تو آپ اِس ای میل کو آرام سے نظر انداز کر سکتے ہیں۔ + + لاگ اِن کرنے کیلئے مندرجہ ذیل لِنک پر کلِک کریں: + %{base_url}/session/email-login/%{email_token} + set_password: + title: "پاسوَرڈ مقرر" + subject_template: "[%{email_prefix}] پاسوَرڈ مقرر کریں" + text_body_template: | + کسی نے [%{site_name}](%{base_url}) پر آپ کے اکاؤنٹ پر پاسوَرڈ مقرر کرنے کیلئے کہا۔ متبادل طور پر، آپ اِس تصدیق کردہ ای میل ایڈریس سے منسلک کسی بھی قابل آن لائن سروس (گُوگل، فَیس بُک، وغیرہ) کا استعمال کرتے ہوئے لاگ اِن کرسکتے ہیں۔ + + اگر آپ نے یہ درخواست نہیں کی، تو آپ اِس ای میل کو آرام سے نظر انداز کر سکتے ہیں۔ + + پاسوَرڈ منتخب کرنے کیلئے مندرجہ ذیل لِنک پر کلِک کریں: + %{base_url}/u/password-reset/%{email_token} + admin_login: + title: "ایڈمن لاگ اِن" + subject_template: "[%{email_prefix}] لاگ اِن" + text_body_template: | + کسی نے [%{site_name}](%{base_url}) پر آپ کے اکاؤنٹ میں لاگ اِن کرنے کیلئے کہا۔ + + اگر آپ نے یہ درخواست نہیں کی، تو آپ اِس ای میل کو آرام سے نظر انداز کر سکتے ہیں۔ + + لاگ اِن کرنے کیلئے مندرجہ ذیل لِنک پر کلِک کریں: + %{base_url}/u/admin-login/%{email_token} + account_created: + title: "اکاؤنٹ تشکیل" + subject_template: "[%{email_prefix}] آپ کا نیا اکاؤنٹ" + text_body_template: | + %{site_name} پر آپ کیلئے ایک نیا اکاؤنٹ بنایا گیا تھا + + نیا پاسوَرڈ منتخب کرنے کیلئے مندرجہ ذیل لِنک پر کلِک کریں: + %{base_url}/u/password-reset/%{email_token} + confirm_new_email: + title: "نئے ای میل کی تصدیق کریں" + subject_template: "[%{email_prefix}] اپنے نئے ای میل ایڈریس کی تصدیق کریں" + text_body_template: | + %{site_name} پر اپنے نئے ای میل ایڈریس کی تصدیق کرنے کیلئے مندرجہ ذیل لِنک پر کلِک کریں: + + %{base_url}/u/authorize-email/%{email_token} + confirm_old_email: + title: "پرانی ای میل تصدیق" + subject_template: "[%{email_prefix}] اپنا موجودہ ای میل ایڈریس تصدیق کریں" + text_body_template: | + اِس سے پہلے کہ ہم آپ کا ای میل ایڈریس تبدیل کر سکیں، ہمیں آپ کے اِس بات کی تصدیق کرنے کی ضرورت ہے کہ آپ کنٹرول کرتے ہیں + موجودہ ای میل اکاؤنٹ۔ آپ کا یہ قدم مکمل کرنے کے بعد، ہم آپ کی طرف سے تصدیق کروائئں گے + نئے ای میل ایڈریس کی۔ + + %{site_name} پر اپنے موجودہ ای میل ایڈریس کی تصدیق کرنے کیلئے مندرجہ ذیل لِنک پر کلک کریں: + + %{base_url}/u/authorize-email/%{email_token} + notify_old_email: + title: "پرانا ای میل مطلع" + subject_template: "[%{email_prefix}] آپ کا ای میل ایڈریس تبدیل ہوگیا ہے" + text_body_template: | + یہ خود کار پیغام ہے کہ آپ کا ای میل ایڈریس + %{site_name} کیلئے تبدیل کر دیا گیا ہے۔ اگر یہ غلطی سے کیا گیا، تو براہ مہربانی رابطہ کریں + ایک سائٹ ایڈمِنِسٹریٹر سے۔ + + آپ کا ای میل ایڈریس جس پر تبدیل کردیا گیا ہے: + + %{new_email} + signup_after_approval: + title: "سائن اَپ بعد از منظوری " + subject_template: "آپ کو %{site_name} پر منظور کر دیا گیا ہے!" + text_body_template: | + %{site_name} میں خوش آمدید! + + ایک سٹاف ممبر نے %{site_name} پر آپ کے اکاؤنٹ کو منظور کیا۔ + + اَب آپ لاگ اِن کرکے اپنے نئے اکاؤنٹ تک رسائی حاصل کرسکتے ہیں: + %{base_url} + + اگر اوپر والا لِنک کلک نہیں ہوتا ہو، تو اپنے ویب براؤزر کے ایڈریس بار میں کاپی اور پَیسٹ کرنے کی کوشش کریں۔ + + %{new_user_tips} + + ہم ہر وقت [مہذب کمیونٹی رویہ](%{base_url}/guidelines) پر یقین رکھتے ہیں۔ + + اپنے قیام کا لطف اٹھائیں! + signup: + title: "سائن اَپ" + subject_template: "[%{email_prefix}] اپنے نئے اکاؤنٹ کی تصدیق کریں" + text_body_template: | + %{site_name} میں خوش آمدید! + + اپنے نئے اکاؤنٹ کی تصدیق اور اُسے چالو کرنے کیلئے مندرجہ ذیل لِنک پر کلِک کریں: + %{base_url}/u/activate-account/%{email_token} + + اگر اوپر والا لِنک کلک نہیں ہوتا ہو، تو اپنے ویب براؤزر کے ایڈریس بار میں کاپی اور پَیسٹ کرنے کی کوشش کریں۔ + page_not_found: + title: "افوہ! وہ صفحہ موجود نہیں یا ذاتی ہے۔" + popular_topics: "مقبول" + recent_topics: "حالیہ" + see_more: "مذید" + search_title: "اِس سائٹ میں تلاش کریں" + search_google: "گُوگَل" + offline: + title: "اَیپ کو لوڈ نہیں کیا جا سکا" + offline_page_message: "لگتا ہے کہ آپ آف لائن ہیں! براہ مہربانی اپنا نیٹ وَرک کنکشن چیک کریں اور دوبارہ کوشش کیجیے۔" + login_required: + welcome_message: | + ## [%{title} پر خوش آمدید](#welcome) + ایک اکاؤنٹ کی ضرورت ہے۔ برائے مہربانی جاری رکھنے کیلئے ایک اکاؤنٹ بنائیں یا لاگ اِن کریں۔ + terms_of_service: + title: "سروس کی شرائط" + signup_form_message: 'میں نے سروس کی شرائط کو پڑھا اور قبول کر لیا ہے۔' + deleted: 'حذف کردہ' + image: "تصویر" + upload: + edit_reason: "تصاویر کی مقامی کاپیاں ڈاؤن لوڈ کریں" + unauthorized: "معذرت، جو فائل آپ اَپ لوڈ کرنے کے کوشش کر رہے ہیں اُس کی اجازت نہیں ہے (اجازت یافتہ ایکسٹینشنز: %{authorized_extensions})۔" + pasted_image_filename: "پَیسٹ کردہ تصویر" + store_failure: "صارف #%{user_id} کیلئے اَپ لوڈ #%{upload_id} سٹور کرنے میں ناکامی" + file_missing: "معذرت، اَپ لوڈ کرنے کیلئے آپ کو ایک فائل فراہم کرنا ضروری ہے۔" + empty: "معذرت، لیکن آپ کی فراہم کردہ فائل خالی ہے۔" + png_to_jpg_conversion_failure_message: "PNG سے JPG پر تبدیل کرتے وقت ایک خرابی کا سامنا کرنا پڑا۔" + attachments: + too_large: "معذرت، جو فائل آپ اَپ لوڈ کرنے کے کوشش کر رہے ہیں وہ بہت بڑی ہے (زیادہ سے زیادہ سائز %{max_size_kb} KB ہے)۔" + images: + too_large: "معذرت، جو فائل آپ اَپ لوڈ کرنے کے کوشش کر رہے ہیں وہ بہت بڑی ہے (زیادہ سے زیادہ سائز %{max_size_kb} KB ہے)، براہ کرم اِس کا سائز تبدیل کریں اور دوبارہ کوشش کریں۔" + larger_than_x_megapixels: "معذرت، جو فائل آپ اَپ لوڈ کرنے کے کوشش کر رہے ہیں وہ بہت بڑی ہے (زیادہ سے زیادہ طول و عرض %{max_image_megapixels}-مَیگا پِکسل ہے)، براہ کرم اِس کا سائز تبدیل کریں اور دوبارہ کوشش کریں۔" + size_not_found: "معذرت، لیکن ہم تصویر کا سائز جان نہیں سکے۔ شاید آپ کی تصویر خراب ہے؟" + placeholders: + too_large: "(%{max_size_kb}KB سے بڑی تصویر)" + avatar: + missing: "معذرت، ہم اس ای میل ایڈریس سے منسلک کوئی اوتار تلاش نہیں کر سکے۔ کیا آپ اُسے دوبارہ اَپ لوڈ کرنے کی کوشش کر سکتے ہیں؟" + flag_reason: + sockpuppet: "ایک نئے صارف نے ایک ٹاپک بنایا، اور اُسی IP ایڈریس (%{ip_address}) پر ایک نئے صارف نے جواب دیا۔ `flag_sockpuppets` سائٹ ترتیب دیکھیے۔" + spam_hosts: "اِس نئے صارف ایک ہی ڈومَین (%{domain}) کے لِنکس کے ساتھ کئی پوسٹس تخلیق کرنے کی کوشش کی۔ `newuser_spam_host_threshold` سائٹ کی ترتیب دیکھیے۔" + email_log: + post_user_deleted: "پوسٹ کا صارف حذف کردیا گیا ہے۔" + no_user: "آئی ڈی %{user_id} کے ساتھ صارف کو تلاش نہیں کیا جا سکا" + anonymous_user: "صارف گمنام ہے" + suspended_not_pm: "صارف معطل ہے، پیغام نہیں ہے" + seen_recently: "صارف کو حال ہی میں دیکھا گیا تھا" + post_not_found: "آئی ڈی %{post_id} کے ساتھ پوسٹ کو تلاش نہیں کیا جا سکا" + notification_already_read: "یہ ای میل جس اطلاع کے بارے میں ہے، وہ پہلے ہی پڑھ لی گئی ہے" + topic_nil: "پوسٹ.ٹاپک نِل ہے" + post_deleted: "پوسٹ مصنف کی طرف سے حذف کر دی گئی" + user_suspended: "صارف معطل کردیا گیا تھا" + already_read: "صارف نے اِس پوسٹ کو پہلے ہی پڑھ لیا ہے" + exceeded_emails_limit: "max_emails_per_day_per_user سے تجاوز کر گیا" + exceeded_bounces_limit: "bounce_score_threshold سے تجاوز کر گیا" + message_blank: "پیغام خالی ہے" + message_to_blank: "پیغام.کیلئے خالی ہے" + text_part_body_blank: "text_part.body خالی ہے" + body_blank: "متن خالی ہے" + no_echo_mailing_list_mode: "صارف کی اپنے پوسٹس کیلئے مَیلنگ لِسٹ اطلاعات غیر فعال ہیں" + color_schemes: + base_theme_name: "بَیس" + default: "لائٹ سکیم" + dark: "ڈارک سکیم" + default_theme_name: "ڈِیفالٹ" + dark_theme_name: "ڈارک" + light_theme_name: "لائٹ" + about: "بارے میں" + guidelines: "ہدایات" + privacy: "پرائیوِیسی" + edit_this_page: " اِس صفحہ میں ترمیم کریں" + csv_export: + boolean_yes: "جی ہاں" + boolean_no: "نہیں " + static_topic_first_reply: | + %{page_name} صفحے کے مواد کو تبدیل کرنے کیلئے اس ٹاپک میں پہلی پوسٹ میں ترمیم کریں۔ + guidelines_topic: + title: "عمومی سوالات کے جوابات / ہدایات" + body: | + + + ## [یہ عوامی بحث مباحثے کیلئے ایک مہذب جگہ ہے](#civilized) + + براہ مہربانی اِس مبابحثے کے فورم پر اُسی احترام سے پیش آئیں جس سے آپ ایک پبلک پارک میں پیش آتے ہیں۔ ہم، بھی، ایک مشترکہ کمیونٹی وسائل ہیں — اور جاری گفتگو کے ذریعہ مہارتیں، علم اور دلچسپیوں کے اشتراک کی ایک جگہ۔ + + یہ پتھر میں لکھے گئے قواعد نہیں ہیں، صرف ہماری کمیونٹی میں کیے جانے والے انسانی فیصلوں کو مدد دینے کیلئے ہدایات اور یہاں مہذب عوامی گفتگو کیلئے ایک صاف اور اچھی طرح سے روشن جگہ محفوض رکھنے کیلئے۔ + + + + ## [بحث کو بہتر بنایے](#improve) + + بحثوں میں بہتری لانے کیلئے ہمیشہ کام، چاہے وہ کتنا ہی چھوٹا کیوں نہ ہو، کر کے ہمیں اِسے مباحثوں کیلئے ایک عظیم جگہ بنانے میں مدد دیں۔ اگر آپ کو یقین نہیں ہے کہ آپ کای اشاعت گفتگو میں کچھ اضافہ کرتی ہے، تو اِس پر غور کریں کہ آپ کیا کہنا چاہتے ہیں اور بعد میں دوبارہ کوشش کیجیے۔ + + یہاں پر بحث کیے گئے موضوعات ہمارے لیے اہم ہیں، اور ہم چاہتے ہیں کہ وہ آپ کیلئے بھی اہم ہوں۔ اِن موضوعات اور ان پر بحث کرنے والوں کا احترام کیجیے، یہاں تک کہ اگر جو کچھ کہا جا رہا ہے آپ اُس سے اتفاق نہ بھی کرتے ہوں، تب بھی۔ + + بحث کو بہتر بنانے کا ایک طریقہ یہ ہے کہ پہلے سے ہوئے جانے والے مباحثوں کو دریافت کریں۔ اپنا جواب دینے یا نیا ٹاپک شروع کرنے سے پہلے یہاں کے ٹاپکس کا ایک جائیزہ لیں، اور پھر آپ کو اپنے آپ سے ملتی جلتی دلچسپی والوں سے ملنے کا بہتر موقع ملے گا۔ + + + + ## [خوشگواررہیں، جب آپ متفق نہ بھی ہوں، تب بھی](#agreeable) + + آپ کسی چیز کا جواب اپنے غیر متفق ہونے کا اظہار کر کے دینا چاہ سکتے ہیں۔ یہ ٹھیک ہے۔ لیکن _خیالات پر تنقید، نہ کہ لوگوں پر_ کرنا یاد رکھیے گا۔ براہ مہربانی بچیں: + + * نام پکارنا + * ذات پر حملے + * کسی اشاعت کے اصلی مواد کے بجائے اُس کے لب و لہجه کا جواب دینا + * جَلد جذباتی تضاد + + اس کے بجائے، معقول جوابی دلائل فراہم کریں جو گفتگو کو بہتر بنائے۔ + + + + ## [آپ کی شرکت سے فرق پڑتا ہے](#participate) + + ہمارے ہیاں مکالمات ہر نئے آنے والے کیلئے لب و لہجه مقرر کرتے ہیں۔ ہمیں اِس کمیونٹی کے مستقبل پر اثر انداز ہونے میں مدد کرنے کیلئے اُن مباحثوں میں مشغول ہونے کا انتخاب کریں جو اِس فورم کو ایک دلچسپ جگہ بناتے ہیں — اور اُن سے دور رہیے جو ایسا نہیں کرتے۔ + + ڈِسکورس ایسے ٹولز فراہم کرتا ہے جو کمیونٹی کو مجموعی طور پر بہترین (اور بدترین) شراکت داروں کی نشاندہی کرنے دیتی ہیں: بُک مارکس، لائیکس، فلَیگز، جوابات، ترامیم، اور اِس طرح دیگر۔ اپنے، اور سبھی دوسروں کے بھی، تجربے کو بہتر بنانے کیلئے اِن ٹولز کا استعمال کریں۔ + + چلیے ہم اپنی کمیونٹی کو بہتر بنا کر چھوڑیں۔ + + + + ## [اگر آپ کوئی مسئلہ دیکھیں، تو اُسے فلَیگ کریں](#flag-problems) + + ماڈریٹرز کو خصوصی اختیار حاصل ہیں؛ وہ اِس فورم کیلئے ذمہ دار ہیں۔ لیکن پھر آپ بھی ہیں۔ آپ کی مدد سے، ماڈریٹرز نہ کہ صرف صفائی کرنے والے یا پولیس بلکہ کمیونٹی سہولت کار بھی بن سکتے ہیں۔ + + جب آپ بد سلوکی دیکھیں تو جواب مت دیں۔ اِس کو تسلیم کرنے سے برے سلوک کی حوصلہ افزائی ہوتی ہے، اپنی توانائی استعمال ہوتی ہے، اور ہر کسی کا وقت ضائع ہوتا ہے۔ _ بس فلَیگ کریں_۔ اگر کافی فلَیگز جمع ہو جائیں، تو خود بخود یا بذریعہ ماڈریٹر مداخلت، کارروائی کر دی جائے گی۔ + + ہماری کمیونٹی کو برقرار رکھنے کیلئے، ماڈریٹرز کسی بھی وقت کسی بھی وجہ سے کسی بھی مواد اور کسی بھی صارف کے اکاؤنٹ کو ہٹانے کا حق محفوظ رکھتے ہیں۔ ماڈریٹرز نئی اشاعتوں کو پیشگی نہیں دیکھتے؛ ماڈریٹرز اور سائٹ کے آپرَیٹرز کمیونٹی کی طرف سے شائع کردہ مواد کیلئے کوئی ذمہ داری نہیں رکھتے۔ + + + + ## [ہمیشہ مہذب رہیے](#be-civil) + + کچھ بھی ایک صحت مند بحث کو ایسے نقصان نہیں پہنچاتا جیسے بد سلوکی پہنچاتی ہے: + + * مہذب رہیے۔ کبھی ایسا کچھ نہ شائع کریں جو ایک مناسب شخص جارحانہ، غیر مہذب، یا نفرت بھری تقریر سمجھے۔ + * اِسے صاف رکھیے۔ کسی بھی فحش یا جنسی طور پر واضح چیز شائع نہ کریں۔ + * ایک دوسرے کی عزت کریں۔ کسی کو ہراساں یا غمگین، لوگوں کی نقالی، یا اُن کی ذاتی معلومات افشاں نہ کریں۔ + * ہمارے فَورم کا احترام کریں۔ سپَیم شائع نہ کریں یا دوسری صورت میں فورم کو وَینڈَلِائز نہ کریں۔ + + یہ عین مطابق تعریفوں کے ساتھ ٹھوس الفاظ نہیں ہیں — اِن چیزوں میں سے کسی ایک کے جیسا _دِکھنے_ سے بھی بچیں۔ اگر آپ کو یقین نہیں ہے، تو اپنے آپ سے پوچھیں کہ آپ کیسا محسوس کریں گے اگر آپ کی پوسٹ نیویارک ٹائمز کے پہلے صفحہ پر نمایاں کی جائے۔ + + یہ ایک پبلک فورَم ہے، اور سرچ انجن اِن مباحثوں کو اِنڈَیکس کرتے ہیں۔ خاندان اور دوستوں کیلئے زبان، لِنکس، اور تصاویر صاف رکھیں۔ + + + + ## [اِسے صاف رکھیے](#keep-tidy) + + چیزوں کو صحیح جگہ پر رکھنے کی کوشش کیا کریں، تاکہ ہم زیادہ وقت بحث مباحثے کرنے میں اور کم وقت صفائی کرنے میں صرف کر سکیں۔ اِس لیے: + + * غلط زُمرہ میں ٹاپک شروع نہ کریں۔ + * ایک ہی چیز کو متعدد ٹاپکس میں کراس-شائع نہ کریں۔ + * بغیر مواد کے جوابات شائع نہ کریں، + * ٹاپک کو درمیان میں بدل کر اُس کا رُخ تبدیلیل نہ کریں۔ + * اپنی اشاعتوں پر دستخط نہ کریں — ہر اشاعت پر آپ کی پروفائل معلومات منسلک ہوتی ہیں۔ + + "+1" یا "متفق" شائع کرنے کے بجائے، لائیک بٹن کا استعمال کریں۔ ایک موجودہ موضوع کو بنیادی طور پر مختلف سمت میں لے جانے کے بجائے، "منسلک ٹاپک کے طور پر جواب دیں" کا استعمال کریں۔ + + + + ## [صرف اپنے خود کی چیزیں شائع کریں](#stealing) + + آپ اجازت کے بغیر ڈیجیٹل کچھ بھی شائع نہیں کرسکتے۔ آپ کسی کی بھی دانشورانہ ملکیت (سافٹ ویئر، ویڈیو، آڈیو، تصاویر)، کو چوری کرنے کے متعلقہ بیانات، لِنکس یا طریقوں کو شائع نہیں کر سکتے ہیں، اور نہ کسی دوسرے قانون کو توڑنے کے متعلقہ۔ + + + + ## [آپ کے مرہونِ مِنَّت](#power) + + یہ سائٹ آپ کے [دوستانہ مقامی سٹاف](/about) اور *آپ*، یہاں کی کمیونٹی، کے ذریعہ چلائی جا رہی ہے۔ چیزوں کو یہاں کس طرح کام کرنا چاہئے کے بارے میں اگر آپ کے پاس مزید سوالات ہیں، تو [سائٹ آراء زُمرہ](/c/site-feedback) میں ایک نیا ٹاپک کھولیں اور چلیے گفتگو کریں! اگر کوئی اہم یا فوری مسئلہ درپیش ہے جو بذریعہ مَیٹا ٹاپک یا فلَیگ کے سنبھالا نہیں جاسکتا، تو [سٹاف صفحہ](/about) کے ذریعہ ہم سے رابطہ کریں۔ + + + + ## [سروس کی شرائط](#tos) + + جی ہاں، قانونی ابہام بَورِنگ ہوتے ہیں، لیکن ہمیں خود کی – اور توسیع کے طور پر، آپ اور آپ کے ڈیٹا – کی غیر دوستانہ افراد سے حفاظت کرنا لاذمی ہے۔ ہمارے پاس [سروس کی شرائط](/tos) ہے جو آپ کے (اور ہمارے) رویے اور مواد، پرائیوِیسی اور قوانین سے متعلقہ حقوق کی تفصیل بیان کرتا ہے۔ اِس سروس کو استعمال کرنے کیلئے، آپ کو ہماری [سروس شرائط](/tos) کی پابندی کرنا ہوگی۔ + tos_topic: + title: "سروس کی شرائط" + body: | + مندرجہ ذیل شرائط و ضوابط ویب سائٹ کے تمام استعمال، اور تمام متن، خدمات اور مصنوعات جو ویب سائٹ پر یا اِس کے ذریعہ دستیاب ہیں، بشمول، لیکن اِن تک محدود نہیں، %{company_domain} فورَم سافٹ ویئر، %{company_domain} سپورٹ فورَم اور %{company_domain} ہوسٹنگ سروس ("ہوسٹنگ")، (ملا کر لیا جائے، تو ویب سائٹ)۔ ویب سائٹ %{company_full_name} ("%{company_name}") کی ملکیت اور اس کی طرف سے چلائی جاتی ہے۔ ویب سائٹ آپ کی منظوری سے مشروط پیش کی جاتی ہے: بغیر کسی ترمیم کے یہاں پر موجود تمام شرائط و ضوابط اور تمام دیگر آپریٹنگ قوانین، پالیسیوں (بشمول، بغیر کسی حد کے، %{company_domain} کی [پرائیوِیسی پالیسی](/privacy) اور [کمیونٹی کے قواعد و ضوابط](/faq)) اور طریقہ کار جو اِس سائٹ پر %{company_name} کی طرف سے وقت بوقت شائع کیے جا سکتے ہیں (مجموعی طور پر، "معاہدہ")۔ + + ویب سائٹ تک رسائی یا اِس کا استعمال کرنے سے پہلے اِس معاہدہ کو احتیاط سے پڑھیں۔ ویب سائٹ کے کسی بھی حصے تک رسائی یا اِس کا استعمال کرنے سے، آپ اِس معاہدے کے شرائط و ضوابط کے پابند ہو جانے پر اتفاق کرتے ہیں۔ اگر آپ اس معاہدے کے تمام شرائط و ضوابط سے متفق نہیں ہیں، تو آپ ویب سائٹ تک رسائی حاصل یا کسی بھی خدمات کا استعمال نہیں کرسکتے ہیں۔ اگر یہ شرائط و ضوابط %{company_name} کی طرف سے ایک پیشکش سمجھی جاتی ہیں، تو قبولیت واضح طور پر اِن شرائط تک محدود ہے۔ ویب سائٹ صرف اُن افراد کیلئے دستیاب ہے جو کم از کم 13 سال کی عمر کے ہیں۔ + + + + ## [1۔ آپ کا %{company_domain} اکاؤنٹ](#1) + + اگر آپ ویب سائٹ پر ایک اکاؤنٹ بناتے ہیں، تو آپ اپنے اکاؤنٹ کو محفوظ رکھنے کے ذمہ دار ہیں اور اِس اکاؤنٹ کے تحت ہونے والی تمام سرگرمیوں کیلئے آپ مکمل طور پر ذمہ دار ہیں۔ آپ کو اپنے اکاؤنٹ کے کسی غیر اجازت استعمال یا سیکورٹی کی کسی دوسری خلاف ورزیاں ہونے پر %{company_name} کو فوری طور پر مطلع کرنا ضروری ہے۔ آپ کی طرف سے کسی بھی کارروائی یا بھول چوک کا ذمہ دار %{company_name} نہیں ہو گا، بشمول اِس طرح کے اعمال یا بھول چوک کے نتیجے میں ہونے والے کسی بھی قسم کے نقصانات۔ + + + + ## [2۔ شراکت داروں کی ذمہ داری](#2) + + اگر آپ ویب سائٹ پر مواد شائع کرتے ہیں، ویب سائٹ پر لِنکس شائع کرتے ہیں، یا دوسری صورت میں خود (یا کسی تیسرے فریق کو) ویب سائٹ کے ذریعہ مواد دستیاب کرتے ہیں (ایسا کوئی بھی مواد، "متن")، آپ مکمل طور پر ذمہ دار ہیں، اُس متن، کے مواد کیلئے اور اُس کے نتیجے میں کسی بھی نقصان کےکیلئے۔ معاملہ ایسا ہی ہے خواہ مواد ٹَیکسٹ، گرافکس، ایک آڈیو فائل، یا کمپیوٹر سافٹ ویئر کا حامل ہو۔ مواد دستیاب کرنے سے، آپ نمائندگی کرتے ہیں اور اِس کی ضمانت دیتے ہیں: + + * مواد کا ڈاؤن لوڈ، کاپی اور استعمال کرنا ملکیت کے حقوق کی خلاف ورزی نہیں کرے گا، بشمول، لیکن اِن تک محدود نہیں، کسی بھی تیسری پارٹی کے، کاپی رائٹ، پَیٹنٹ، ٹریڈ مارک یا تجارتی رازوں کے حقوق؛ + * اگر آپ کے آجر کے پاس آپ کی دانشورانہ ملکیت پر حق حاصل ہے، تو آپ نے یا تو (i) متن کو شائع کرنے یا اُسے دستیاب کرنے کیلئے اپنے آجر سے اجازت موصول کی ہوئی ہے، بشمول کسی بھی سافٹ ویئر لیکن اِس تک محدود نہیں، یا (ii) اپنے آجر سے متن میں یا اُس پر سے تمام حقوق کیلئے چھوٹ حاصل کر لی ہے؛ + * آپ نے متن سے متعلق کسی بھی تیسرے فریق لائسنسوں کے ساتھ مکمل طور پر عمل کیا ہے، اور ضروری شرائط کو صارفین تک کامیابی سے منتقل کرنے کیلئے تمام ضروری چیزوں کو پورا کر لیا ہے؛ + * کوئی بھی وائرس، وَورمز، میل ویئر، ٹروجن گھوڑے یا دیگر نقصان دہ یا تباہ کن مواد، متن میں شامل نہیں یا اِنہیں انسٹال نہیں کرتا؛ + * متن سپَیم نہیں ہے، مشین یا رَینڈم طور پر پیدا نہیں کیا گیا ہے، اور اِس میں غیر اخلاقی یا ناپسندیدہ تجارتی مواد شامل نہیں ہے جن سے ٹریفک تیسری فریقین کی سائٹس پر بھیجی جائے یا تیسری فریقین سائٹس کی سرچ انجنوں پر درجہ بندی بہتر کی جائے، یا مزید غیر قانونی کارروائیوں کو بڑھانے کیلئے تیار کیا گیا ہے (جیسے کہ فیشِنگ) یا وصول کنندگان کو مواد کے ذریعہ کے بارے میں گمراہ کرتا ہے (جیسے کہ سپُوفِنگ)؛ + * متن فحش نہیں ہے، دھمکیوں پر مشتمل نہیں ہے یا تشدد پر مشتعل نہیں کرتا، اور کسی بھی تیسری فریق کی پرائیوِیسی یا اشاعت کے حقوق کی خلاف ورزی نہیں کرتا؛ + * آپ کا متن ناپسندیدہ الیکٹرانک پیغامات کے ذریعہ مشتہر نہیں کیا جا رہا جیسے کہ نیوز گروپوں، ای میل فہرستوں، بلاگز اور ویب سائٹس پر سپَیم لنکس، اور اِنہی طرح کے دوسرے بِن مانگے پروموشنل طریقوں کے ذریعہ؛ + * آپ کے متن کو اِس طرح سے نام نہیں دیا گیا جس سے پڑھنے والے گمراہ ہو جائیں اور سمجھیں کہ آپ کوئی دوسرے شخص یا کمپنی ہیں؛ اور + * آپ نے، ایسے متن کے معاملے میں جس میں کمپیوٹر کَوڈ شامل ہو، درست طریقے سے درجہ بندی کیی گئی ہے اور/یا مواد کی قِسم، نوعیت، استعمالات اور اثرات بیان کیے ہیں، چاہے %{company_name} کی طرف سے ایسا کرنے کی درخواست کی گئی یا نہ کی گئی ہو۔ + + + + ## [3۔ صارف مواد لائسنس](#3) + + صارف کی شراکتیں [کَرِی اَیٹِو کامنز اَیٹریبیوشن-غیر تجارتی-شئیر الائیک 3.0 اَن پورٹِڈ لائسنس](http://creativecommons.org/licenses/by-nc-sa/3.0/deed.en_US) کے طرز پر لائسنس شدہ ہیں۔ اِن نمائندوں یا وارنٹیوں میں سے کسی کو بھی محدود کیے بغیر، %{company_name} کے پاس حق (اگرچہ ذمہ داری نہیں) ہے کہ، %{company_name} کی واحد صوابدید پر (i) کسی بھی متن سے انکار یا اُس کو ہٹا دیا جائے کہ، %{company_name} کی مناسب رائے میں، %{company_name} کی کسی بھی پالیسی کی خلاف ورزی ہو رہی ہے یا کسی بھی طرح سے نقصان دہ یا قابل اعتراض ہے، یا (ii) کسی بھی فرد یا ادارے کیلئے کسی بھی وجوہات کی بنا پر ویب سائٹ تک رسائی یا اُس کے استعمال کو ختم یا اُس کا انکار کیا جا سکتا ہے، %{company_name} کی واحد صوابدید پر۔ کسی بھی پہلے سے ادا کی گئی رقم کی واپسی کی ذمہ داری %{company_name} پر نہیں ہوگی۔ + + + + + ## [4۔ ادائیگی اور تجدید](#4) + + ### جنرل شرائط + + ویب سائٹ پر قیمت والی اختیاری خدمات یا اَپ گریڈ دستیاب ہوسکتے ہیں۔ ایک اختیاری قیمت والی خدمت یا اَپ گریڈ کا استعمال کرتے ہوئے، آپ ظاہر کی گئی ماہانہ یا سالانہ سبسکرِپشن فیس %{company_name} کو ادا کرنے کیلئے ااتفاق کرتے ہیں۔ ادائیگیاں پری-پَیڈ بنیاد پر وصول کی جائیں گی، جس دن آپ سروس یا اَپ گریڈ کا استعمال کرنا شروع کرتے ہیں اور ماہانہ یا سالانہ سبسکرِپشن مدت کے حساب سے جیسا کہ مطلع کیا گیا ہو۔ یہ فیسیں قابلِ واپسی نہیں ہیں۔ + + ### خود بخود تجدید + + جب تک آپ قابل اطلاق سبسکرِپشن مدت کے اختتام سے پہلے %{company_name}کو مطلع نہیں کرتے کہ آپ کسی سروس یا اَپ گریڈ کو منسوخ کرنا چاہتے ہیں، آپ کی سبسکرپشن خود بخود تجدید کر دی جائے گی اور آپ ہمیں پھر قابل اطلاق سالانہ یا ماہانہ سبسکرپشن فیس (اس کے ساتھ ساتھ کسی بھی ٹیکس) کو، بذریعہ کریڈٹ کارڈ یا کسی اور ادائیگی کے طریقہ کے جو ہمارے پاس ریکارڈ پر موجود ہو، وصول کرنے کی اجازت دیتے ہیں۔ کسی بھی وقت سبسکرِپشنیں منسوخ کر دی جا سکتی ہیں۔ + + + + ## [5۔ خدمات](#5) + + ### ہَوسٹِنگ، سپورٹ خدمات + + %{company_name} کی طرف سے اختیاری ہَوسٹِنگ اور سپورٹ خدمات ہر ایسی ایک سروس کی الگ شرائط و ضوابط کے تحت فراہم کی جاسکتی ہیں۔ ہَوسٹِنگ/سپورٹ یا سپورٹ خدمات کے اکاؤنٹ کیلئے سائن اَپ کر کہ، آپ ایسی شرائط و ضوابط کی پابندی کرنے کیلئے اتفاق کرتے ہیں۔ + + ### HTTPS + + ہم HTTPS ایک ادائیگی والے ایَڈ آن کے طور پر پیش کرتے ہیں۔ سائن اَپ کرنے اور %{company_domain} پر اپنی مرضی کا ڈَومَین استعمال کر کہ، آپ ہمیں آپ کی سائٹ پر HTTPS فراہم کرنے کے واحد مقصد کیلئے ڈَومَین نام رجسٹر کرنے والے کی جانب سے (مثال کے طور پر ضروری سرٹیفکیٹ کی درخواست کرنے کیلئے) عمل کرنے کی اجازت دیتے ہیں۔ + + ### اِنٹرپرائز + + %{company_name} کی طرف سے انٹرپرائز ہَوسٹِنگ خدمات ہر ایسی ایک سروس کی الگ شرائط و ضوابط، جو کسٹمر مخصوص معاہدے سے متعین کی جاتی ہیں، کے تحت فراہم کی جاتی ہیں۔ ایک انٹرپرائز ہَوسٹِنگ اکاؤنٹ کیلئے سائن اَپ کر کہ، آپ ایسی شرائط و ضوابط کی پابندی کرنے کیلئے اتفاق کرتے ہیں۔ + + + + ## [6۔ ویب سائٹ کے زائرین کی ذمہ داری](#6) + + %{company_name} نے جائزہ نہیں لیا ہے، اور نہ ہی لے سکتا ہے، ویب سائٹ پر شائع کیے جانے والے تمام مواد، سمیت کمپیوٹر سافٹ ویئر، اور لہٰذا اس مواد کے متن، استعمال یا اثرات کیلئے ذمہ دار نہیں ہوسکتا ہے۔ اِس ویب سائٹ کو چلانے سے، %{company_name} یہاں پر موجود مواد کی توثیق کرنے کی نمائندگی یا اشارہ نہیں کرتا، یا نہ اِس کا یقین ہے کہ یہ مواد درست، مفید یا غیر نقصان دہ ہے۔ اپنے آپ اور اپنے کمپیوٹر سسٹموں کو وائرس، وَورمز، ٹروجن گھوڑے یا دیگر نقصان دہ یا تباہ کن مواد سے بچانے کیلئے، احتیاطی تدابیر اختیار کرنے کے آپ خود ذمہ دار ہیں۔ ویب سائٹ میں ایسا مواد جو جارحانہ، نامناسب، یا دوسری صورت میں قابل اعتراض ہو، شامل ہوسکتا ہے، اور اِس کے ساتھ ساتھ مواد میں تکنیکی غلطیاں، ٹائپینگ کی غلطیاں، اور دیگر غلطیاں بھی شامل ہو سکتی ہیں۔ ویب سائٹ میں ایسا مواد بھی شامل ہو سکتا ہے جو پرائیوِیسی یا اشاعت کے حقوق کی خلاف ورزی کرتا ہو، یا دانشورانہ ملکیت اور دیگر ملکیت کے حقوق کی خلاف ورزی کرتا ہو، تیسرے فریقوں کے، یا جن کا ڈاؤن لوڈ، کاپی یا استعمال کرنا، بیان کردہ یا غیر بیان شدہ، اضافی شرائط و ضوابط کے تابع ہے۔ زائرین کی طرف سے ویب سائٹ کے استعمال، یا یہاں شائع کردہ مواد کے اِن افراد کی طرف سے کسی بھی ڈاؤن لوڈ کرنے سے ہونے والے کسی بھی نقصان سے %{company_name} ہر قِسم کی ذمہ داری سے دستبردار ہے۔ + + + + ## [7۔ دوسری ویب سائٹ پر شائع کردہ مواد](#7) + + ہم نے جائزہ نہیں لیا ہے، اور نہ ہی لے سکتے ہیں، بذریعہ ویب سائٹس اور ویب صفحات کے دستیاب کردہ، سمیت کمپیوٹر سافٹ ویئر تمام مواد کا، جن کو %{company_domain} لِنک کرتا ہے اور جو %{company_domain} کو لِنک کرتے ہیں۔ اِن غیر-%{company_domain} ویب سائٹس اور ویب صفحات پر %{company_name} کا کوئی کنٹرول نہیں ہے، اور ان کے مواد یا ان کے استعمال کا ذمہ دار نہیں ہے۔ کسی غیر-%{company_domain} ویب سائٹ یا ویب پیج پر لِنک کر کے، %{company_name} ایسی ویب سائٹ یا ایسے ویب صفحہ کی نمائندگی یا توثیق کرنے کا اشارہ نہیں کرتا۔ اپنے آپ اور اپنے کمپیوٹر سسٹموں کو وائرس، وَورمز، ٹروجن گھوڑے یا دیگر نقصان دہ یا تباہ کن مواد سے بچانے کیلئے، احتیاطی تدابیر اختیار کرنے کے آپ خود ذمہ دار ہیں۔ غیر-%{company_domain} ویب سائٹس اور ویب صفحات کے استعمال کے نتیجے میں ہونے والے کسی بھی نقصان سے %{company_name} ہر قِسم کی ذمہ داری سے دستبردار ہے۔ + + + + ## [8۔ کاپی رائٹ خلاف ورزی اور DMCA پالیسی](#8) + + جیسا کہ %{company_name} دوسروں کو اپنے دانشورانہ ملکیت کے حقوق کا احترام کرنے کیلئے کہتا ہے، اُسے طرح یہ دوسروں کے دانشورانہ ملکیت کے حقوق کا بھی احترام کرتا ہے۔ اگر آپ سمجھتے ہیں کہ %{company_domain} پر موجود یا لِنک کیے جانے والا مواد آپ کے کاپی رائٹ کی خلاف ورزی کرتا ہے، اور اگر یہ ویب سائٹ امریکہ میں رہتی ہے، تو آپ کو %{company_name} کی [ڈیجیٹل ملینیم کاپی رائٹ ایکٹ](http://en.wikipedia.org/wiki/Digital_Millennium_Copyright_Act) ("DMCA") پالیسی کے مطابق %{company_name} کو مطلع کرنا چاہئے۔ %{company_name} اِس طرح کے تمام نوٹسوں پر کارروائی کرے گا، بشمول جیسا کہ مطلوب یا مناسب ہو خلاف ورزی کرنے والے مواد کو ہٹانے یا اُس کے تمام لِنکس کو غیر فعال کر کہ۔ اگر، مناسب حالات میں، ایک وِزِیٹر کی طرف سے %{company_name} کے یا دوسروں کے کاپی رائٹ یا دیگر دانشورانہ ملکیت کے حقوق کی بار بار خلاف ورزی کرنے کا تعین کیا جاتا ہے تو %{company_name} اُس شخص کی ویب سائٹ تک رسائی اور اِس کا استعمال برطرف کر دے گا۔ ایسی برطرفی کی صورت میں، %{company_name} کو کسی بھی پہلے سے ادا کی گئی رقم کی واپسی کی ذمہ داری %{company_name} پر نہیں ہوگی۔ + + + + ## [9۔ دانشورانہ پراپرٹی](#9) + + یہ معاہدہ آپ کو %{company_name} سے %{company_name} کی یا کسی تیسرے فریق کی دانشورانہ ملکیت منتقل نہیں کرتا ہے، اور اِس پراپرٹی میں اور پر تمام حق، ٹائیٹل اور انٹرسٹ (فریقین کے درمیان) صرف %{company_name} کا پاس ہی رہیں گے۔ %{company_name}، %{company_domain}، %{company_domain} کا لَوگَو، اور %{company_domain} یا ویب سائٹ کے ساتھ استعمال ہونے والے دیگر تمام ٹریڈ مارک، سروس کے نشان، گرافکس اور لَوگَو، %{company_name} یا %{company_name} کے لائسنس دینے والوں کے ٹریڈ مارک یا رجسٹرڈ ٹریڈ مارکس ہیں۔ ویب سائٹ کے ساتھ استعمال ہونے والے دیگر ٹریڈ مارک، سروس کے نشان، گرافکس اور لَوگَو دوسری تیسرے فریق کے ٹریڈ مارک ہو سکتے ہیں۔ آپ کا ویب سائٹ کا استعمال آپ کو کسی بھی %{company_name} یا تیسرے فریق کے ٹریڈ مارک کو دوبارہ بنانے یا استعمال کرنے کیلئے حق یا لائسنس فراہم نہیں کرتا ہے۔ + + + + ## [10۔ انتساب](#10) + + %{company_name} آپ کے صفحے کے فوٹر میں '%{company_domain} کے مرہونِ مِنَّت'، تھِیم مصنف، اور فونٹ انتساب جیسے انتساب کے لِنکس کو ظاہر کرنے کا حق محفوظ رکھتا ہے۔ + + + + ## [11۔ تبدیلیاں](#11) + + %{company_name} اِس معاہدے کے کسی بھی حصے میں ترمیم یا تبدیل کرنے کے، اپنی واحد صوابدید پر، حق محفوظ رکھتا ہے۔ یہ آپ کی ذمہ داری ہے کہ اس معاہدے کو باقاعدگی سے تبدیلیوں کیلئے چیک کرتے رہیں۔ اِس معاہدے میں کسی بھی تبدیلی کی اشاعت کے بعد، آپ کی طرف سے ویب سائٹ کا استعمال جاری رکھنا یا اُس تک رسائی حاصل کرنا، اِن تبدیلیوں کو قبول کرنے کے برابر ہے۔ %{company_name}، مستقبل میں، ویب سائٹ کے ذریعہ نئی خدمات اور/یا خصوصیات (بشمول، نئے ٹُولز اور وسائل کے تعارف کے) بھی پیش کر سکتا ہے۔ ایسی نئی خصوصیات اور/یا خدمات اس معاہدے کی شرائط و ضوابط کے تابع ہوں گی۔ + + + + ## [12۔ برطرفی](#12) + + %{company_name}، کسی وجہ کے ساتھ یا اُس کے بغیر، کسی نوٹس کے ساتھ یا اُس کے بغیر، کسی بھی وقت پوری ویب سائٹ یا اُس کے کسی بھی خاص حصے تک آپ کی رسائی فوری طور پر موثر طریقے سے برطرف کر سکتا ہے۔ اگر آپ اِس معاہدے یا اپنے %{company_domain} اکاؤنٹ (اگر آپ کے پاس ہے تو) کو ختم کرنا چاہتے ہیں، آپ کو صرف ویب سائٹ کا استعمال کرنا بند کرنا ہو گا۔ اس معاہدے کی تمام شرائط جو اپنی فطرت میں برطرفی کے باوجود ختم نہیں ہونی چاہئیں، وہ بچ جائیں گی، بشمول بغیر کسی حد کے، ملکیت کی شرائط، وارنٹی کے ڈِسکلَیمر، ہرجانہ اور ذمہ داریوں کی حدیں۔ + + + + ## [13۔ وارنٹیوں کے ڈِسکلَیمر](#13) + + ویب سائٹ "جیسے ہے ویسے ہی" کے طور پر فراہم کی جاتی ہے۔ %{company_name} اور اِس کے سپلائرز اور لائسنس دینے والے کسی بھی قِسم، اظہار یا اشارہ کی گئیں، بشمول، بغیر کسی حد کے، فروختگی کی ضمانتیں، کسی خاص مقصد کیلئے صحیح اور غیر خلاف ورزی کی تمام وارنٹیوں کا انکار کرتے ہیں۔ نہ %{company_name} اور نہ ہی اِس کے سپلائرز اور لائسنس دینے والے، اِس بات کی ضمانت دیتے ہیں کہ ویب سائٹ غلطی سے پاک ہو گی یا اِس تک رسائی مسلسل یا بلاتعطل رہے گی۔ اگر آپ واقعی یہ پڑھ رہے ہیں، تو یہ [ایک تحفہ] (http://www.newyorker.com/online/blogs/shouts/2012/12/the-hundred-best-lists-of-all-time.html) ہے۔ آپ کو سمجھ ہے کہ آپ ویب سائٹ کے ذریعہ ڈاؤن لوڈ کرتے ہیں، یا مواد یا خدمات حاصل کرتے ہیں، اپنی صوابدید اور خطرے کے پر۔ + + + + ## [14۔ ذمہ داری کی حد](#14) + + کسی بھی واقعہ پر %{company_name}، یا اِس کے سپلائرز اور لائسنس دینے والے، اِس معاہدے کے کسی بھی موضوع کے بارے میں کسی بھی معاہدے کے اندر، غفلت، سخت ذمہ داری یا دوسرے قانونی یا متوازن نظریہ کے تحت ذمہ دار نہیں ہوں گے: (i) کسی خاص، حادثاتی یا نتیجتاً نقصانات؛ (ii) متبادل مصنوعات یا خدمات کی خریداری کی قیمت؛ (iii) استعمال میں تعطلی یا ڈیٹا میں نقصان یا خرابی؛ یا (iv) کارروائی کے سبب سے پہلے بارہ (12) مہینے کے عرصے کے دوران اِس معاہدے کے تحت آپ کی طرف سے %{company_name} کو ادا کی گئیں فیسوں سے زیادہ کسی بھی رقم کیلئے۔ اُن کے مناسب کنٹرول سے باہر معاملات کی وجہ سے کسی بھی ناکامی یا تاخیر کیلئے %{company_domain} پر کوئی ذمہ داری نہیں ہوگی۔ پہلے آنے والا قابل اطلاق قانون کی طرف سے منع کیے جانے کی حد تک لاگو نہیں ہو گا۔ + + + + ## [15۔ جنرل نمائندگی اور وارنٹی](#15) + + آپ نمائندگی کرتے ہیں اور اس بات کی ضمانت دیتے ہیں کہ (i) آپ کا ویب سائٹ کا استعمال %{company_name} کی [پرائیویسی پالیسی](/privacy)، [کمیونٹی کے قواعد و ضوابط](/guidelines)، اِس معاہدے کے ساتھ اور تمام قابل اطلاق قوانین اور قواعد و ضوابط (بشمول بغیر کسی حد کے آپ کے ملک، ریاست، شہر، یا دیگر سرکاری علاقے میں کسی بھی مقامی قوانین یا قواعد و ضوابط، آن لائن رویہ اور قابل قبول مواد کے بارے میں، اور ملک، جس میں یہ ویب سائٹ رہتی ہے یا جس میں آپ رہتے ہیں، سے برآمد کردہ تکنیکی ڈَیٹا کی منتقلی سے متعلقہ تمام قابل اطلاق قوانین سمیت) کے ساتھ سخت مطابقت رکھے گا اور (ii) آپ کی ویب سائٹ کا استعمال کسی بھی تیسرے فریق کی دانشورانہ ملکیت کے حقوق کی خلاف ورزی یا اُنہیں خورد برد نہیں کرے گا۔ + + + + ## [16۔ تلافی](#16) + + تلافی کرنے اور %{company_name}، اِس کے ٹھیکیداروں، لائسنس دینے والوں کو، اور اِن کے متعلقہ ڈائریکٹرز، افسران، ملازمین اور ایجنٹوں کو تمام دعووں اور اخراجات، بشمول اٹارنیوں کی فیسیں، آپ کی ویب سائٹ کے استعمال سے پیدا ہونے والے، بشمول، لیکن اِس تک محدود نہیں، اِس معاہدے کی خلاف ورزی پر ہونے والے نقصانات کی ذمہ داری سے برطرف کرنے کیلئے آپ اتفاق کرتے ہیں۔ + + + + ## [17۔ متفرقہ](#17) + + یہاں کے موضوع کے معاملہ میں یہ معاہدے %{company_name} اور آپ کے درمیان مکمل معاہدہ کو تشکیل دیتا ہے، اور وہ صرف %{company_name} کے ایک بااختیار اَیگزَیکِٹِو کی جانب سے دستخط کردہ ایک تحریری ترمیم، یا %{company_name} کی طرف سے شائع کیے جانے والے ایک رِیوائز کردہ ورژن کی طرف سے ترمیم کیا جا سکتا ہے۔ قابل اطلاق قانون کی حد کے علاوہ، اگر کوئی ہو، دوسری صورت میں فراہم کرتا ہے، یہ معاہدے، ویب سائٹ تک رسائی یا استعمال ریاستِ کَیلِیفَورنیا، یو ایس اے، کے قوانین کی مطابق ہو گا، قانون کے کانفلِکٹ دفعات کو چھوڑ کر، اور کسی بھی تنازعہ کیلئے مناسب مقام سان فرانسِسکَو کاؤنٹی، کَیلِیفَورنیا میں واقع ریاست اور وفاقی عدالتیں ہوں گی۔ امتناعی یا منصفانہ رَیلیف کے دعووں یا دانشورانہ ملکیت کے حقوق کے دعووں (جو کسی بھی قابل عدالت میں کسی بانڈ کی فراہمی کے بغیر لائے جا سکتے ہیں) کے علاوہ، اِس معاہدے کے تحت ہونے والے کسی بھی تنازعہ کو آخر میں جیوڈیشیل آربریٹَیشن اور مَیڈیشن سروس، انکارپوریٹڈ ("JAMS") کے جامع ثالثی قواعد کے مطابق، ایسے قواعد کے مطابق مقرر کردہ تین ثالثین کی طرف سے حل کیا جائے گا۔ ثالثی انگریزی زبان میں سان فرانسِسکَو، کَیلِیفَورنیا میں ہوگی اور کسی بھی عدالت میں فیصلہ نافذ کیا جاسکتا ہے۔ اس معاہدے کو نافذ کرنے کیلئے کسی بھی عمل یا کارروائی میں موجودہ فریق کو اخراجات اور وکلاء کی فیسوں کا حق حاصل ہوگا۔ اگر اس معاہدے کے کسی بھی حصہ کو غلط یا غیر قابلِ عمل متعین کیا جاتا ہے، تو یہ حصہ فریقین کے اصل ارادے کی عکاسی کرتے ہوئے کے طور پر سمجھا جائے گا، اور باقی حصے مکمل ذور اور اثر کے ساتھ رہیں گے۔ اِس معاہدے کی کسی بھی اصطلاح یا شرط کے کسی بھی فریق کی طرف سے، کسی ایک دفعہ کی چھوٹ، اِس طرح کی اصطلاح یا شرط کی اِس کے بعد اگلی خلاف ورزی کیلئے چھوٹ نہیں فراہم کرے گی۔ آپ اس معاہدے کے تحت اپنے حقوق کو کسی بھی فریق کو تفویض کرسکتے ہیں جو اِس کی شرائط و ضوابط کے ساتھ رضامندی، اور اُن پر پابند رہنے کیلئے اتفاق کریں؛ اِس معاہدے میں %{company_name} بغیر کسی شرط کے تحت اپنے حقوق کو تفویض کرسکتا ہے۔ یہ معاہدے فریقین، اُن کے جانشینوں اور اجازت یافتہ تفویض کنندہ پر پابندی اور اُن کے فائدہ کیلئے ہو گا۔ + + یہ دستاویز CC-BY-SA ہے۔ یہ آخری دفعہ جنوری 1، 2018 کو اَپ ڈیٹ کی گئی تھی۔ + + اصل میں [وَرڈپرَیس کی سروس کی شرائط](http://en.wordpress.com/tos/) سے اخذ کردہ۔ + privacy_topic: + title: "پرائیوِیسی پالیسی" + body: | + + + ## [ہم کیا معلومات جمع کرتے ہیں؟](#collect) + + ہم آپ سے معلومات اکھٹی کرتے ہیں جب آپ ہماری ویب سائٹ پر رجسٹر کرتے ہیں اور ڈیٹا جمع کرتے ہیں جب آپ یہاں پر شئیر کردہ مواد کو پڑھ، لکھ، اور اس کا جائزہ لے کر اِس فورَم میں حصہ لیتے ہیں۔ + + ہماری سائٹ پر رجسٹر کرتے وقت، آپ سے اپنا نام اور ای میل پتہ درج کرنے کیلئے کہا جا سکتا ہے۔ تاہم، آپ رجسٹریشن کے بغیر ہماری سائٹ وِزِٹ کر سکتے ہیں۔ آپ کا ای میل ایڈریس کی تصدیق بذریعہ ایک منفرد لنک پر مشتمل ای میل سے ہوگی۔ اگر وہ لنک وِزِٹ کیا جاتا ہے، تو ہم جان جائیں گے کہ آپ اس ای میل ایڈریس کو کنٹرول کرتے ہیں۔ + + رجسٹر کرنے کے بعد کچھ شائع کرنے پر، ہم اشاعت کا تخلیق کنندہ IP ایڈریس ریکارڈ کر لیتے ہیں۔ ہم سرور لاگز کو بھی برقرار رکھ سکتے ہیں جس میں ہمارے سرور پر کیے جانے والی ہر درخواست کا IP ایڈریس شامل ہوتا ہے۔ + + + + ## [ہم آپ کی معلومات کس لیے استعمال کرتے ہیں؟](#use) + + ہم آپ سے جمع کردہ کوئی بھی معلومات مندرجہ ذیل طریقوں میں سے ایک میں استعمال کر سکتے ہیں: + + * آپ کے اِس سائٹ پر تجربے کو ذاتی بنانے کیلئے — آپ کی معلومات ہمیں آپ کی ذاتی ضروریات کو بہتر سمجھنے میں مدد دیتی ہے۔ + * اِس سائٹ کو بہتر بنانے کیلئے — ہم مسلسل آپ کی طرف سے موصول ہونے والی معلومات اور آراء کی بنیاد پر سائٹ خصوصیات کو بہتر بنانے کی کوشش کرتے رہتے ہیں۔ + * کسٹمر سروس کو بہتر بنانے کیلئے — آپ کی معلومات ہمیں کسٹمر سروس کی درخواستوں اور معاونت کی ضروریات کا زیادہ موثر طریقے سے جواب دینے میں ہماری مدد کرتی ہے۔ + * ہر مخصوص مدت کے بعد ای میلز بھیجنے کیلئے — آپ کی طرف سے فراہم کردہ ای میل ایڈریس استعمال کیا جا سکتا ہے، آپ کو بھیجنے کیلئے: معلومات، اطلاعات جن کی آپ ٹاپکس میں تبدیلیاں ہونے پر، یا اپنے صارف نام کے جواب میں درخواست کرتے ہیں، انکوائریوں کے جواب کیلئے، اور/یا دیگر درخواستوں یا سوالات کا جواب دینے کیلئے۔ + + + + ## [ہم آپ کی معلومات کی حفاظت کیسے کرتے ہیں؟](#protect) + + جب آپ اپنی ذاتی معلومات درج، جمع، یا ایکسَیس کرتے ہیں تو ہم آپ کی ذاتی معلومات کو محفوظ رکھنے کیلئے مختلف قِسم کے سیکورٹی اقدامات کو لاگو کرتے ہیں۔ + + + + ## [آپ کی ڈَیٹا برقرار رکھنے کی پالیسی کیا ہے؟](#data-retention) + + ہم نیک نیتی کے ساتھ ممکنہ کوشش کریں گے کہ: + + * اِس سرور پر کیے جانے والی تمام درخواستوں کے IP ایڈریسوں پر مشتمل سرور لاگز کو 90 دنوں سے زائد برقرار نہیں رکھیں گے۔ + * رجسٹر جردہ صارفین اور اُن کی اشاعتوں کے ساتھ منسلک IP ایڈریسوں کو 5 سال سے زائد برقرار نہیں رکھیں گے۔ + + + + ## [کیا ہم کوکیز کا استعمال کرتے ہیں؟](#cookies) + + جی ہاں۔ کُوکِیز ایسی چھوٹی فائلیں ہوتی ہیں جو ایک سائٹ یا اُس کی سروس فراہم کرنے والے، بذریعہ ویب براؤزر آپ کے کمپیوٹر کی ہارڈ ڈرائیو میں منتقل کر دیتے ہیں (اگر آپ کی اجازت ہو)۔ یہ کُوکِیز سائٹ کو آپ کے براؤزر کو پہچاننے کے قابل بناتے ہیں اور، اگر آپ کے پاس ایک رجسٹر کردہ اکاؤنٹ ہو، تو براؤزر کو آپ کے رجسٹرڈ اکاؤنٹ سے منسلک کر دیتے ہیں۔ + + ہم مستقبل کے وِزِٹس کیلئے آپ کی ترجیحات کو سمجھنے اور محفوظ کرنے کیلئے کُوکِیز کا استعمال کرتے ہیں اور سائٹ پر ٹریفک اور سائٹ اِنٹرَیکشن کے بارے میں مجموعی ڈَیٹا مرتب کرتے ہیں تاکہ ہم مستقبل میں بہتر سائٹ پر تجربات اور ٹولز پیش کرسکیں۔ ہم تیسرے فریق سروس فراہم کرنے والوں کے ساتھ معاہدے کرسکتے ہیں تاکہ ہماری ویب سائٹس پر آنے والوں کو بہتر سمجھا جا سکے۔ یہ سروس فراہم کرنے والوں کو اجازت نہیں ہے کہ ہماری جانب سے جمع کردہ معلومات کو ہمارے کام کو چلنے اور بہتر بنانے میں ہماری مدد کرنے کے علاوہ کسی اور مقصد کیلئے استعمال کریں۔ + + + + ## [کیا ہم بیرونی تنظیموں کو کوئی بھی معلومات ظاہر کرتے ہیں؟](#disclose) + + ہم آپ کی ذاتی طور پر قابلِ شناخت معلومات کی فروخت، تجارت یا بیرونی تنظیموں کو منتقل نہیں کرتے ہیں۔ ِاس میں بھروسہ والی تیسری پارٹیاں شامل نہیں ہیں جو ہماری سائٹ کو چلانے میں مدد کرتی ہیں، ہمارے کام کو چلانے یا آپ کی خدمت کرنے میں مدد کرتی ہیں، جب تک کہ یہ تنظیمیں آپ کی معلومات کو رازداری سے رکھنے پر متفق رہیں۔ ہم آپ کی معلومات اِس صورت میں بھی جاری کرسکتے ہیں جب ہم یقین رکھتے ہوں کہ ایسا کرنا قانون پر عمل کرنے کیلئے مناسب ہے، ہماری ویب سائٹ کی پالیسیوں کو نافذ کرنے کیلئے، یا ہمارے یا دوسروں کے حقوق، ملکیت، یا حفاظت کو محفوظ رکھنے کیلئے بھی۔ تاہم، مارکیٹنگ، اشتہارات، یا دیگر استعمال کیلئے غیر ذاتی طور پر قابلِ شناخت وِزِیٹر معلومات دیگر تنظیموں کو فراہم کی جا سکتی ہے۔ + + + + ## [تیسری پارٹی والے لِنکس](#third-party) + + کبھی کبھار، ہماری صوابدید پر، ہم اپنی ویب سائٹ پر تیسری پارٹی کی مصنوعات یا خدمات شامل یا پیش کرسکتے ہیں۔ اِن تیسری پارٹی سائٹس کی الگ اور آزادانہ پرائیوِیسی پالیسیاں ہیں۔ لہٰذا ہم اِن منسلک سائٹس کے مواد اور سرگرمیوں کیلئے کسی قِسم کی بھی کوئی ذمہ داری نہیں رکھتے۔ اس کے باوجود، ہم اپنی سائٹ کی سالمیت کی حفاظت کرنے کی کوشش کرتے ہیں اور اِن سائٹس کے بارے میں کسی بھی رائے کا خیر مقدم کرتے ہیں۔ + + + + ## [بچوں کی آن لائن پرائیوِیسی کے تحفظ کے ایکٹ کے تعمیل](#coppa) + + ہماری سائٹ، مصنوعات اور خدمات سبھی اُن لوگوں کیلئے پیش کی جاتی ہیں جو کم از کم 13 سال یا اُس سے زائد عمر کے ہیں۔ اگر یہ سرور امریکہ میں ہے، اور آپ 13 سال سے کم عمر ہیں، تو COPPA ([بچوں کی آن لائن پرائیوِیسی کے تحفظ کا ایکٹ](https://en.wikipedia.org/wiki/Children%27s_Online_Privacy_Protection_Act)) کی ضروریات کے مطابق، آپ یہ سائٹ استعمال نہ کیجیے۔ + + + + ## [آن لائن پرائیویسی پالیسی صرف](#online) + + یہ آن لائن پرائیویسی پالیسی صرف ہماری ویب سائٹ کے ذریعہ جمع کردہ معلومات پر لاگو ہوتی ہے اور اُس معلومات پر نہیں جو آف لائن جمع کی جائے۔ + + + + ## [آپ کی رضامندی](#consent) + + ہماری ویب سائٹ کا استعمال کرتے ہوئے، آپ ہماری سائٹ کی پرائیویسی پالیسی پر رضامندی کا اظہار کرتے ہیں۔ + + + + ## [ہماری پرائیویسی پالیسی میں تبدیلیاں](#changes) + + اگر ہم اپنی پرائیویسی پالیسی کو تبدیل کرنے کا فیصلہ کریں، تو ہم اِس صفحہ پر اُن تبدیلیوں کو شائع کریں گے۔ + + یہ دستاویز CC-BY-SA ہے۔ یہ آخری دفعہ مئی 31، 2013 کو اَپ ڈیٹ کی گئی تھی۔ + badges: + editor: + name: ترمیم کنندہ + description: پہلی پوسٹ ترمیم + long_description: | + یہ بَیج پہلی دفعہ اپنی اشاعتوں میں سے ایک میں ترمیم کرنے پر دیا جاتا ہے۔ تاہم آپ ہمیشہ کیلئے اپنی اشاعتوں میں ترمیم نہیں کر سکیں گے، ترامیم کرنا ہمیشہ ایک اچھا خیال ہوتا ہے — آپ اپنے پیغامات کو بہتر بنا سکتے ہیں، چھوٹی غلطیوں کو درست کر سکتے ہیں، یا ایسا کچھ بھی شامل کریں جو آپ پہلی دفعہ میں شامل کرنا بھول گئے تھے۔ اپنی اشاعتیں کو مذید بہتر بنانے کیلئے اُن کو ترمیم کیجیے! + basic_user: + name: بَیسِک + description: تمام لازمی کمیونٹی کے افعال عطا کر دیے گئے + long_description: | + یہ بَیج آپ کے ٹرسٹ لَیول 1 تک پہنچنے پر دیا جاتا ہے۔ تھوڑی دیر کیلئے یہاں رکنے اور ہماری کمیونٹی کے بارے میں جاننے کیلئے چند ٹاپک پڑھنے کیلئے شکریہ۔ آپ پر نئے صارف کی پابندیاں ہٹا دی گئیں ہیں؛ آپ کو تمام لازمی کمیونٹی صلاحیات، جیسے کہ ذاتی پیغام رسانی، فلَیگ سازی، وِیکی ترمیم، اور ایک سے زیادہ تصاویر اور لِنکس شائع کرنا، عطا کر دی گئیں ہیں۔ + member: + name: ممبر + description: دعوت نامے، گروپ پیغام رسانی، مزید لائیکس عطا کر دیے گئے + long_description: | + یہ بَیج کے آپ ٹرسٹ لَیول 2 تک پہنچنے پر دیا جاتا ہے۔ ہماری کمیونٹی میں واقعی شامل ہونے کیلئے چند ہفتوں کیلئے حصہ لینے کیلئے شکریہ۔ اَب آپ اپنے صارف صفحے یا انفرادی ٹاپکس سے دعوت نامہ بھیج سکتے ہیں، گروپ ذاتی پیغامات تشکیل دے سکتے ہیں، اور آپ کو فی دن کچھ مذید لائیکس حاصل ہو گئے ہیں۔ + regular: + name: رَیگولر + description: زُمرہ دوبارہ درج کرنا، نام تبدیل کرنا، فَولَو کردہ لِنکس، وِیکی، مذید لائیکس عطا کر دیے گئے + long_description: | + یہ بَیج آپ کے ٹرسٹ لَیول 3 تک پہنچنے پر دیا جاتا ہے۔ مہینوں کی مدت کیلئے ہماری کمیونٹی باقاعدگی سے حصہ بننے کیلئے شکریہ۔ اَب آپ سب سے زیادہ سرگرم پڑھنے والوں میں سے ایک ہیں، اور ایک قابل اعتماد شراکت دار ہیں جو ہماری کمیونٹی کو زبردست بنا دیتے ہیں۔ اَب آپ ٹاپکس کے زُمرہ جات اور نام تبدیل کر سکتے ہیں، مزید طاقتور سپَیم فلَیگز کا فائدہ اٹھا سکتے ہیں، ایک ذاتی لاؤنج تک رسائی حاصل کرسکتے ہیں اور آپ کو فی دن کئی زیادہ لائیکس بھی حاصل ہو گئے ہیں۔ + leader: + name: لِیڈر + description: گلَوبل ترمیم، پِن، بند، آرکائیو، تقسیم اور ضم کرنا، مزید لائیکس عطا کر دیے گئے + long_description: | + یہ بَیج آپ کے ٹرسٹ لَیول 4 تک پہنچنے پر دیا جاتا ہے۔ آپ اِس کمیونٹی میں سٹاف کی طرف سے منتخب کردہ ایک رہنما کی حیثیت رکھتے ہیں، اور آپ نے اپنے اعمال اور الفاظ سے باقی کمیونٹی کیلئے ایک مثبت مثال قائم کی ہے۔ آپ کے پاس تمام پوسٹس میں ترمیم کرنے کی صلاحیت ہے، ٹاپک پر ماڈریٹر کے عام افعال جیسے کہ پِن، بند، غیر فہرست شدہ، آرکائیو، تقسیم، اور ضم کرنا، اور آپ کے پاس فی دن ٹنَوں لائیکس ہیں۔ + welcome: + name: خوش آمدید + description: لائیک موصول ہوا + long_description: | + یہ بَیج آپ کے اپنی پوسٹ پر پہلا لائیک موصول ہونے پر دیا جاتا ہے۔ مبارک ہو، آپ نے کچھ شائع کیا ہے جو آپ کے ساتھی کمیونٹی ممبران کو دلچسپ، زبردست، یا مفید معلوم ہوا ہے! + autobiographer: + name: آپ بيتی نويس + description: پروفائل معلومات کو پُر کر دیا + long_description: | + یہ بَیج اپنی صارف پروفائل بھرنے اور پروفائل تصویر منتخب کرنے پر عطا کیا جاتا ہے۔ کمیونٹی کو بتانے سے کہ آپ کون ہیں اور کن چیزوں میں دلچسپی رکھتے ہیں، ایک بہتر، مذید مشترکہ کمیونٹی بنانے میں مدد دیتا ہے۔ ہمارے ساتھ شامل ہوں! + anniversary: + name: سالگرٰہ + description: ایک سال کیلئے سرگرم رکن، کم از کم ایک دفعہ پوسٹ کیا + long_description: | + یہ بَیج آپ کے ایک سال کیلئے رکن ہونے کے ساتھ ساتھ اُسی سال کم از کم ایک پوسٹ شائع کرنے پر عطا کیا جاتا ہے۔ یہاں رکنے اور ہماری کمیونٹی میں حصہ لینے کیلئے آپ کا شکریہ۔ ہم آپ کے بغیر یہ نہیں کر سکتے تھے۔ + nice_post: + name: اچھا جواب + description: ایک جواب پر 10 لائیکس موصول ہوئے + long_description: |+ + یہ بَیج آپ کے جواب پر 10 لائیکس موصول ہونے پر عطا کیا جاتا ہے۔ آپ کا جواب کمیونٹی پر واقعی اثر انداز ہوا اور گفتگو کو آگے بڑھانے میں مدد کا بائث بنا! + + good_post: + name: عمدہ جواب + description: ایک جواب پر 25 لائیکس موصول ہوئے + long_description: | + یہ بَیج آپ کے جواب پر 25 لائیکس موصول ہونے پر عطا کیا جاتا ہے۔ آپ کا جواب غیر معمولی تھا اور گفتگو کو سب کیلئے بہتر بنانے کا بائث بنا! + great_post: + name: زبردست جواب + description: ایک جواب پر 50 لائیکس موصول ہوئے + long_description: | + یہ بَیج آپ کے جواب پر 50 لائیکس موصول ہونے پر عطا کیا جاتا ہے۔ زبردست! آپ کا جواب حوصلہ افزانے والا، انتہائی دلچسپ، مزاحیہ یا بصیرت شعار تھا اور کمیونٹی نے اُسے بہت پسند کیا۔ + nice_topic: + name: اچھا ٹاپک + description: ایک ٹاپک پر 10 لائیکس موصول ہوئے + long_description: | + یہ بَیج آپ کے ٹاپک پر 10 لائیکس موصول ہونے پر عطا کیا جاتا ہے۔ ارے، آپ نے ایک دلچسپ گفتگو شروع کی جس سے کمیونٹی لطف اندوز ہوئی! + good_topic: + name: عمدہ ٹاپک + description: ایک ٹاپک پر 25 لائیکس موصول ہوئے + long_description: | + یہ بَیج آپ کے ٹاپک پر 25 لائیکس موصول ہونے پر عطا کیا جاتا ہے۔ آپ نے ایک متحرک گفتگو شروع کی جس میں کمیونٹی نے بھرپور حصہ لیا اور جس سے بہت پسند کیا گیا! + great_topic: + name: زبردست ٹاپک + description: ایک ٹاپک پر 50 لائیکس موصول ہوئے + long_description: | + یہ بَیج آپ کے ٹاپک پر 50 لائیکس موصول ہونے پر عطا کیا جاتا ہے۔ آپ نے ایک انتہائی دلچسپ گفتگو کا آغاز کیا اور کمیونٹی نے اُس کے نتیجے میں چلنے والی متحرک بحث کا لطف اٹھایا! + nice_share: + name: اچھا شیئر + description: 25 منفرد زائرین کے ساتھ ایک اشاعت شیئر کی + long_description: | + یہ بَیج ایک ایسا لِنک جس پر 25 بیرونی زائرین نے کلِک کیا ہو شیئر کرنے پر عطا کیا جاتا ہے۔ ہماری گفتگوئوں، اور اِس کمیونٹی کے بارے میں بات پھیلانے کیلئے شکریہ۔ + good_share: + name: عمدہ شیئر + description: 300 منفرد زائرین کے ساتھ ایک اشاعت شیئر کی + long_description: | + یہ بَیج ایک ایسا لِنک جس پر 300 بیرونی زائرین نے کلِک کیا ہو شیئر کرنے پر عطا کیا جاتا ہے۔ عمدہ کام! آپ نے نئے لوگوں کے ایک گروپ کو ایک شاندار بحث دکھائی اور اِس کمیونٹی کو بڑھنے میں مدد دی ہے۔ + great_share: + name: زبردست شیئر + description: 1000 منفرد زائرین کے ساتھ ایک اشاعت شیئر کی + long_description: | + یہ بَیج ایک ایسا لِنک جس پر 1000 بیرونی زائرین نے کلِک کیا ہو شیئر کرنے پر عطا کیا جاتا ہے۔ زبردست! آپ نے نیا ناظرین کی ایک بہت بڑی تعداد میں ایک دلچسپ بحث کو فروغ دیا ہے، اور ہماری کمیونٹی کو بڑے پیمانے پر بڑھانے میں مدد دی ہے! + first_like: + name: پہلا لائیک + description: ایک پوسٹ کو لائیک کیا + long_description: | + یہ بَیج آپ کو :heart: کا بٹن استعمال کرتے ہوئے پہلی مرتبہ پوسٹ لائیک کرنے پر عطا کیا جاتا ہے۔ پوسٹس لائیک کرنا ایک عمدہ طریقہ ہے تاکہ آپ کے ساتھی کمیونٹی ممبران کو پتہ چل سکے کہ اُن کا شائع کیا ہوا دلچسپ، مفید، زبردست، یا پُر لطف تھا۔ محبت بانٹیے! + first_flag: + name: پہلا فلَیگ + description: ایک پوسٹ کو فلَیگ کیا + long_description: | + یہ بَیج آپ کو پہلی مرتبہ پوسٹ فلَیگ کرنے پر عطا کیا جاتا ہے۔ فلَیگز کے ذریعہ ہم مل کر ہر ایک کیلئے اِس جگہ کو صاف، اور اچھی طرح سے روشن جگہ رکھنے میں مدد دیتے ہیں۔ اگر آپ کسی بھی ایسی پوسٹ کو نوٹس کرتے ہیں جس پر کسی بھی وجہ سے ماڈریٹر کی توجہ کی ضرورت ہوتی ہے تو براہ کرم فلَیگ کرنے سے نہ ہچکچائیں۔ اگر آپ ساتھی صارفین کی پوسٹس کے ساتھ کوئی مسئلہ دیکھتے ہیں تو آپ اُنہیں ایک ذاتی پیغام بھیجنے کیلئے بھی فلَیگ کرسکتے ہیں۔ اگر آپ کو کوئی مسئلہ نظر آتا ہے، تو اُسے :flag_black: فلَیگ کریں! + promoter: + name: پروموٹر + description: ایک صارف کو دعوت دی + long_description: | + آپ کو اپنے صارف صفحے یا کسی ٹاپک کے نچلے حصے پر بذریعہ مدعو کے بٹن کسی کو کمیونٹی میں شامل ہونے کیلئے دعوت دینے پر یہ بَیج عطا کیا جاتا ہے۔ دوست جو مخصوص بحثوں میں دلچسپی رکھتے ہوں، اُن کو دعوت دینا ہماری کمیونٹی کے ساتھ نئے لوگوں کو متعارف کرانے کا ایک عمدہ طریقہ ہے، تو شکریہ! + campaigner: + name: مہم چلانے والا + description: 3 بَیسِک صارفین کو مدعو کیا + long_description: | + جب آپ نے 3 ایسے لوگوں کو مدعہ کیا ہو جنہوں نے بعد میں بَیسِک صارفین بننے کیلئے سائٹ پر کافی وقت گزارا ہو، تو یہ بَیج عطا کیا جاتا ہے۔ ایک متحرک کمیونٹی کو باقاعدگی سے نئے آنے والوں کی، جو باقاعدگی سے شرکت اور گفتگو میں نئی آوازیں شامل کرتے ہیں، کی ضرورت ہوتی ہے۔ + champion: + name: چَیمپیئن + description: 5 ممبران کو دعوت دی + long_description: | + جب آپ نے 5 ایسے لوگوں کو مدعہ کیا ہو جنہوں نے بعد میں مکمل ممبر بننے کیلئے سائٹ پر کافی وقت گزارا ہو، تو یہ بَیج عطا کیا جاتا ہے۔ زبردست! نئے اراکین کے ساتھ ہماری کمیونٹی کی جداگانیت کو بڑھانے کیلئے شکریہ! + first_share: + name: پہلا شیئر + description: ایک پوسٹ شیئر کی + long_description: | + جب آپ پہلی دفعہ شیئر بٹن کا استعمال کرتے ہوئے کسی جواب یا ٹاپک کا لِنک شیئر کرتے ہیں، تو یہ بَیج عطا کیا جاتا ہے۔ لِنکس شیئر کرنا باقی دنیا کے ساتھ دلچسپ مباحثوں کو ظاہر کرنے اور اپنی کمیونٹی کو بڑھانے کا ایک عمدہ طریقہ ہے۔ + first_link: + name: پہلا لِنک + description: دوسرے ٹاپک کا لِنک شامل کیا + long_description: | + یہ بَیج آپ کے پہلی دفعہ کسی دوسرے ٹاپک کا لِنک شامل کرنے پر عطا کیا جاتا ہے۔ ٹاپکس کو لِنک کرنے سے ٹاپکس کے درمیان دونوں طرف کے کنکشن ظاہر ہوتے ہیں، جس سے ساتھی پڑھنے والوں کو متعلقہ دلچسپ مباحثے ڈھونڈنے میں مدد ملتی ہے۔ آزادانہ طور پر لِنک کیا کریں! + first_quote: + name: پہلا اقتباس + description: ایک پوسٹ کا اقتباس کیا + long_description: | + یہ بَیج آپ کو اپنے جواب میں پہلی دفعہ کسی پوسٹ کا اقتباس کرنے پر عطا کیا جاتا ہے۔ اپنے جواب میں پچھلی پوسٹس کے متعلقہ حصوں کا حوالہ دینے سے مباحثوں کو ایک دوسرے سے منسلک اور موضوع پر رہنے میں مدد ملتی ہے۔ اقتباس کرنے کا سب سے آسان طریقہ ایک پوسٹ کے سیکشن کو اجاگر کرنا، اور پھر کسی بھی جواب کے بٹن کو دبانا ہے۔ ل کھول کر اقتباس کریں! + read_guidelines: + name: ہدایات پڑھِیں + description: کمیونٹی کے قواعد و ضوابط پڑھیں + long_description: | + یہ بَیج کمیونٹی کے قواعد و ضوابط پڑھنے پر عطا کیا جاتا ہے۔ اِن سادہ ہدایات پر عمل اور اُنہیں شیئر کرنے سے ہر ایک کیلئے ایک محفوظ، پُر لطف اور پائیدار کمیونٹی بنانے میں مدد ملتی ہے۔ ہمیشہ یاد رکھیے کہ اِس سکرین کی دوسری طرف بھی ایک، آپ سے کافی ملتا جلتا، انسان ہے۔ لہاز رکھیے! + reader: + name: پڑهنے والا + description: 100 سے زائد جوابات کے ساتھ ٹاپک میں ہر جواب پڑھا + long_description: | + یہ بَیج آپ کو پہلی مرتبہ 100 سے زائد جوابات والے ایک طویل ٹاپک کو پڑھنے پر عطا کیا جاتا ہے۔ گفتگو کو قریب سے پڑھنے پر آپ کو مدد ملتی ہے بحث کی پیروی کرنے اور مختلف نقطہ نظر کو سمجھنے میں، اور مذید دلچسپ مباحث کی طرف پہنچ جاتے ہیں۔ آپ جتنا زیادہ پڑھتے ہیں، گفتگو اتنی زیادہ بہتر ہو جاتی ہے۔ جیسا کہ ہم کہنا پسند کرتے ہیں، پڑھنا بنیادی ہے! :slight_smile: + popular_link: + name: مقبول لنک + description: 50 کلکس کے ساتھ ایک بیرونی لِنک پوسٹ کیا + long_description: | + یہ بَیج آپ کی طرف سے ایک شیئر کردہ لِنک پر 50 کلکس ہونے پر عطا کیا جاتا ہے۔ ایک مفید لِنک جس نے گفتگو میں دلچسپ سیاق و سباق شامل کیا، شائع کرنے کیلئے شکریہ! + hot_link: + name: ہاٹ لِنک + description: 300 کلکس کے ساتھ ایک بیرونی لِنک پوسٹ کیا + long_description: | + یہ بَیج آپ کی طرف سے ایک شیئر کردہ لِنک پر 300 کلکس ہونے پر عطا کیا جاتا ہے۔ ایک انتہائی دلچسپ لِنک شائع کرنے کیلئے شکریہ جس نے گفتگو کو آگے بڑھایا اور بحث پر روشنی ڈالی! + famous_link: + name: مشہور لِنک + description: 1000 کلکس کے ساتھ ایک بیرونی لِنک پوسٹ کیا + long_description: | + یہ بَیج آپ کی طرف سے ایک شیئر کردہ لِنک پر 1000 کلکس ہونے پر عطا کیا جاتا ہے۔ زبردست! آپ نے ایک ایسا لِنک شائع کیا جس نے ضروری تفصیل، سیاق و سباق، اور معلومات شامل کرکے گفتگو کو نمایاں طور پر بہتر بنایا۔ شاندار کام! + appreciated: + name: قدر کی گئی + description: 20 پوسٹس پر 1 لائیک موصول ہوا + long_description: | + یہ بَیج آپ کو 20 مختلف پوسٹس پر کم از کم ایک لائیک موصول ہونے پر عطا کیا جاتا ہے۔ کمیونٹی یہاں کی مباحثوں میں آپ کی شرکت سے لطف اندوز ہو رہی ہے! + respected: + name: محترم + description: 100 پوسٹس پر 2 لائیکس موصول ہوئے + long_description: | + یہ بَیج آپ کو 100 مختلف پوسٹس پر کم از کم 2 لائیکس موصول ہونے پر عطا کیا جاتا ہے۔ کمیونٹی یہاں کے مباحثوں میں آپ کی کئی شراکتوں کی وجہ سے آپ کا احترام کرنے لگی ہے۔ + admired: + name: تعریف کردہ + description: 300 پوسٹس پر 5 لائیکس موصول ہوئے + long_description: | + یہ بَیج آپ کو 300 مختلف پوسٹس پر کم از کم 5 لائیکس موصول ہونے پر عطا کیا جاتا ہے۔ زبردست! کمیونٹی آپ کی یہاں کے مباحثوں میں مسلسل، اعلیٰ معیار کی شراکتوں کی تعریف کرتی ہے۔ + out_of_love: + name: محبت کی لِمٹ سے باہر + description: ایک دن میں 50 لائیکس استعمال کر لیے + long_description: | + یہ بَیج آپ کے اپنے فی دن کے تمام 50 لائیکس استعمال کرلینے پر عطا کیا جاتا ہے۔ ایک لمحہ کیلئے رُک کر جن اشاعتوں سے آپ لطف اندوز اور جن کی آپ تعریف کرتے ہیں، اُنہیں لائیک کرنا یاد رکھنے سے آپ کے ساتھی کمیونٹی ممبران کی حوصلہ افزائی ہوتی ہے کہ وہ مستقبل میں مذید عمدہ مباحث تخلیق کریں۔ + higher_love: + name: عظیم تر محبت + description: ایک دن میں 50 لائیکس 5 مرتبہ استعمال کر لیے + long_description: | + یہ بَیج آپ کے اپنے فی دن کے تمام 50 لائیکس 5 دنوں کیلئے استعمال کرلینے پر عطا کیا جاتا ہے۔ بہترین مباحثوں کی ہر دن مسلسل حوصلہ افزائی کرنے کیلئے وقت نکالنے کا شکریہ! + crazy_in_love: + name: محبت میں دیوانہ + description: ایک دن میں 50 لائیکس 20 مرتبہ استعمال کر لیے + long_description: | + یہ بَیج آپ کے اپنے فی دن کے تمام 50 لائیکس 20 دنوں کیلئے استعمال کرلینے پر عطا کیا جاتا ہے۔ زبردست! آپ باقاعدگی سے آپنے ساتھی کمیونٹی ممبران کی حوصلہ افزائی کرنے کی ایک مثال ہیں! + thank_you: + name: آپ کا شکریہ + description: 20 لائیک کردہ پوسٹس ہیں اور 10 لائیکس دیے + long_description: | + یہ بَیج آپ کے پاس 20 لائیک کردہ پوسٹس ہونے اور جواب میں 10 یا اُس سے زائد لائیکس دینے پر عطا کیا جاتا ہے۔ جب کوئی آپ کی پوسٹس کو لائیک کرتا ہے، تو آپ بھی دوسروں کی اشاعتوں کو لائیک کرنے کیلئے وقت نکال لیتے ہیں۔ + gives_back: + name: واپس دیتا ہے + description: 100 لائیک کردہ پوسٹس ہیں اور 100 لائیکس دیے + long_description: | + یہ بَیج آپ کے پاس 100 لائیک کردہ پوسٹس ہونے اور جواب میں 100 یا اُس سے زائد لائیکس دینے پر عطا کیا جاتا ہے۔ اِن کو آگے بڑھانے کیلئے شکریہ! + empathetic: + name: خیال رکھنے والا + description: 500 لائیک کردہ پوسٹس ہیں اور 1000 لائیکس دیے + long_description: | + یہ بَیج آپ کے پاس 500 لائیک کردہ پوسٹس ہونے اور جواب میں 1000 یا اُس سے زائد لائیکس دینے پر عطا کیا جاتا ہے۔ زبردست! آپ سخاوت اور باہمی تعریف کی ایک مثال ہیں :two_hearts:۔ + first_emoji: + name: پہلی اِیمَوجی + description: ایک پوسٹ میں اِیمَوجی کا استعمال کیا + long_description: | + یہ بَیج آپ کے اپنی پوسٹ پر پہلی مرتبہ اِیمَوجی شامل کرنے پر عطا کیا جاتا ہے :thumbsup:۔ اِیمَوجی آپ کو اپنی پوسٹس میں جذبات کا اظہار کرنے دیتی ہیں، خوشی :smiley: سے اداسی :anguished: تک، اداسی سے غصہ :angry: تک اور سب کچھ درمیان میں :sunglasses:۔ سینکڑوں انتخابات :ok_hand: میں سے منتخب کرنے کیلئے صرف : (کالن) ٹائپ کیجیے یا اَیڈیٹر میں اِیمَوجی ٹُول بار کے بٹن کو دبائیے۔ + first_mention: + name: پہلا ذکر + description: پوسٹ میں ایک صارف کا ذکر کیا + long_description: اس بَیج آپ کے اپنی پوسٹ میں پہلی مرتبہ کسی کا @صارفنام ذکر کرنے پر عطا کیا جاتا ہے۔ ہر ذکر اُس شخص کیلئے ایک اطلاع پیدا کرتا ہے، تاکہ اُنہیں آپ کی پوسٹ کے بارے میں پتہ چل سکے۔ کسی بھی صارف، یا اگر اجازت ہو تو، گروپ کا ذکر کرنے کیلئے صرف @ (at کا نشان) ٹائپ کرنا شروع کریں – یہ اُن کی توجہ میں کچھ لانے کا آسان طریقہ ہے۔ + first_onebox: + name: پہلا وَن باکس + description: ایک وَن باکس کردہ لِنک شائع کیا + long_description: یہ بَیج عطا کیا جاتا ہے جب آپ پہلی مرتبہ ایک لِنک کو لائین میں اکیلا شائع کرتے ہیں، جس کے بعد وہ خود بخود ایک مختصر خلاصہ، ایک عنوان، اور (جب دستیاب ہو) ایک تصویر کے ساتھ ایک وَن باکس میں کھل جاتا ہے۔ + first_reply_by_email: + name: بذریعہ ای میل پہلا جواب + description: ایک پوسٹ پر بذریعہ ای میل جواب دیا + long_description: | + یہ بَیج آپ کے پہلی مرتبہ ایک پوسٹ پر بذریعہ ای میل جواب دینے پر عطا کیا جاتا ہے :e-mail:۔ + new_user_of_the_month: + name: "مہینہ کے سب سے زیادہ قابل قدر نئے صارف" + description: اِن کے پہلے مہینے میں شاندار شراکتَیں + long_description: | + یہ بَیج ہر ماہ دو نئے صارفین کو اپنی شاندار مجموعی شراکتوں کیلئے مبارکباد دینے کیلئے عطا کیا جاتا ہے، جیسا کہ اُن کی پوسٹس کو کتنی کثرت سے اور کن کی طرف سے لائیک کیا گیا تھا، سے ناپا جاتا ہے۔ + enthusiast: + name: پرجوش + description: 10 دن وِزِٹ کیا + long_description: یہ بَیج مسلسل 10 دنوں تک وِزِٹ کرنے پر عطا کیا جاتا ہے۔ ہمارے ساتھ ایک ہفتے سے زائد رکنے کیلئے شکریہ! + aficionado: + name: اَفیشیاناڈو + description: 100 دن وِزِٹ کیا + long_description: یہ بَیج مسلسل 100 دنوں تک وِزِٹ کرنے پر عطا کیا جاتا ہے۔ یہ تین ماہ سے زائد ہے! + devotee: + name: جاں نثار + description: 365 دن وِزِٹ کیا + long_description: یہ بَیج مسلسل 365 دنوں تک وِزِٹ کرنے پر عطا کیا جاتا ہے۔ واہ، ایک پورا سال! + badge_title_metadata: "%{site_title}پر %{display_name} بَیج " + admin_login: + success: "ای میل بھیج دی گئ" + errors: + unknown_email_address: "نامعلوم ای میل ایڈریس۔" + invalid_token: "غلط ٹوکن۔" + email_input: "ایڈمن اِی میل" + submit_button: "اِیمیل بھیجیں" + performance_report: + initial_post_raw: اِس ٹاپک میں آپ کی سائٹ کیلئے روزانہ کی کارکردگی کی رپورٹیں شامل ہیں۔ + initial_topic_title: ویب سائٹ کارکردگی کی رپورٹیں + tags: + title: "ٹیگز" + staff_tag_disallowed: "\"%{tag}\" ٹَیگ صرف سٹاف کی طرف سے لاگو کیا جا سکتا ہے۔" + staff_tag_remove_disallowed: "\"%{tag}\" ٹَیگ صرف سٹاف کی طرف سے ہٹایا جا سکتا ہے۔" + rss_by_tag: "ٹاپکس %{tag} کے ساتھ ٹَیگ کردہ" + finish_installation: + congratulations: "مبارک باد، آپ نے ڈِسکورس اِنسٹال کر لیا!" + register: + button: "رجسٹر" + title: "رجسٹر ایڈمن اکاؤنٹ" + help: "شروع کرنے کے لئے ایک نیا اکاؤنٹ رجسٹر کریں" + no_emails: "بدقسمتی سے، سَیٹ اَپ کے دوران کوئی ایڈمِنِسٹریٹر ای میل واضح نہیں کی گئیں، لہٰذا ترتیب کو حتمی شکل دینا مشکل ہوسکتا ہے۔" + confirm_email: + title: "اپنے ای میل کی تصدیق کریں" + message: "

    ہم نے
    %{email} پر ایک ایکٹیویشن میل بھیج دی ہے۔ براہ مہربانی اپنے اکاؤنٹ کو چالو کرنے کیلئے میل میں دی گئی ہدایات پر عمل کریں۔

    اگر یہ آپ کو موصول نہ ہو، تو یقینی بنائیں کہ آپ نے ڈِسکورس کیلئے صحیح طریقہ سے ای میل سَیٹ کر لی ہے اور اپنا سپَیم فولڈر چیک کریں۔

    " + resend_email: + title: "ایکٹیویشن اِیمیل دوبارہ بھیجییں" + message: "

    ہم نے %{email} پر ایکٹیویشن اِیمیل دوبارہ بھیج دی ہے" + safe_mode: + title: "سَیف مَوڈ میں داخل ہوں" + description: "پلگ اِن یا سائٹ کو اپنی مرضی کے حساب سے بنانے کیلئے کیے جانے والی تبدیلیوں کو لَوڈ کیے بغیر، سَیف مَوڈ آپ کو اپنی سائٹ کی جانچ پڑتال کرنے دیتا ہے۔" + no_customizations: "موجودہ تھِیم کو غیر فعال کریں" + only_official: "غیر آفیشل پلگ اِن غیر فعال کریں" + no_plugins: "تمام پلگ اِن غیر فعال کریں" + enter: "سَیف مَوڈ میں داخل ہوں" + must_select: "سَیف مَوڈ میں داخل ہونے کیلئے آپ کا کم از کم ایک آپشن منتخب کرنا ضروری ہے۔" + wizard: + title: "ڈِسکورس سَیٹ اَپ" + step: + locale: + title: "اپنے ڈِسکورس میں خوش آمدید!" + fields: + default_locale: + description: "آپ کی کمیونٹی کیلئے ڈِیفالٹ زبان کونسی ہے؟" + forum_title: + title: "نام" + description: "آپ کا نام ایک فاصلے سے نظر آجانے والی چیز ہے، ممکنہ زائرین کی طرف سے آپ کی کمیونٹی کے بارے میں نوٹ کیے جانے والی پہلی چیز۔ آپ کا نام اور عنوان آپ کی کمیونٹی کے بارے میں کیا کہتا ہے؟" + fields: + title: + label: "آپ کی کمیونٹی کا نام" + placeholder: "جمیلہ کے ٹھہرنے کی جگہ" + site_description: + label: "اپنی کمیونٹی کو ایک مختصر جملہ میں بیان کریں" + placeholder: "جمیلہ اور اس کی دوستوں کیلئے دلچسپ چیزوں پر بحث کرنے کی ایک جگہ" + introduction: + title: "تعارف" + fields: + welcome: + label: "خیرمقدم ٹاپک" + description: "

    آپ ایک لفٹ میں ہوتے ہوئے 1 منٹ کے اندر اندر کسی اجنبی کو اپنی کمیونٹی کس طرح سے بیان کریں گے؟

    • یہ مباحثے کن لوگوں کیلئے ہیں؟
    • مجھے یہاں پر کیا مل سکتا ہے؟
    • مجھے یہاں کیوں آنا چاہئے؟

    آپ کا خیرمقدم ٹاپک وہ پہلی چیز ہے جسے نئے آنے والے پڑھیں گے۔ اِسے اپنی ایک پیراگراف پر مشتمل 'لفٹ میں اِلتجاء' یا 'مِشَن کے بیان' کے طور پر تصور کیجیے۔

    " + one_paragraph: "براہ مہربانی اپنے خیر مقدم پیغام کو ایک پیراگراف تک محدود رکھیں۔" + privacy: + title: "رسائی" + description: "

    کیا آپ کا کمیونٹی سب کے لئے کھلی ہے، یا یہ رکنیت، دعوت ناموں، یا منظوری کے ذریعہ محدود ہے؟ اگر آپ پسند کریں تو، آپ ذاتی طور پر چیزوں کو مقرر کر سکتے ہیں، اور پھر بعد میں کمیونٹی کو عوامی طور پر کھول سکتے ہیں۔

    آپ ہمیشہ ٹاپکس، یا اپنے صارف پروفائل صفحے سے بھی دعوت نامے بھیج سکتے ہیں۔

    " + fields: + privacy: + choices: + open: + label: "عوامی" + description: "کوئی بھی اِس کمیونٹی تک رسائی حاصل کر سکتا ہے اور ایک اکاؤنٹ کیلئے سائن اَپ کرسکتا ہے" + restricted: + label: "ذاتی" + description: "جنہیں میں نے مدعو کیا یا منظوری دی ہے صرف وہ لوگ اِس کمیونٹی تک رسائی حاصل کر سکتے ہیں" + contact: + title: "رابطہ" + fields: + contact_email: + label: "میل" + placeholder: "name@example.com" + description: "اِس کمیونٹی کیلئے ذمہ دار شخص یا گروپ کا ای میل ایڈریس۔ اہم اطلاعات جیسے کہ غیر نمٹائے گئے فلَیگز، سیکورٹی اپ ڈیٹس، اور فوری کمیونٹی رابطے کیلئے آپ کے \"بارے میں\" والے صفحے پر اہم اطلاعات کیلئے استعمال کیا جاتا ہے۔" + contact_url: + label: "وِیب صفحہ" + placeholder: "http://www.example.com/contact-us" + description: "آپ یا آپ کے ادارہ کیلئے عام رابطہ کیلئے وَیب صفحہ۔ آپ کے \"بارے میں\" والے صفحے پر دکھایا جائے گا۔" + site_contact: + label: "خودکار پیغامات" + description: "تمام خودکار ڈِسکورس ذاتی پیغامات اِس صارف کی طرف سے بھیجے جائیں گے جیسے کہ فلَیگ انتباہات اور بیک اَپ تکمیل نوٹس۔" + corporate: + title: "ادارہ" + description: "یہ نام آپ کی پرائیوَیسی پالیسی اور سروس کی شرائط میں درج کیے جائیں گے، جو ٹاپک ہیں جو کہ آپ سٹاف زُمرہ میں ترمیم کرسکتے ہیں۔ اگر آپ کی کمپنی نہیں ہے، تو ابھی کیلئے آپ آسانی سے اِس مرحلے کو چھوڑ سکتے ہیں۔" + fields: + company_short_name: + label: "کمپنی نام (مختصر)" + placeholder: "اِینیٹَیک" + company_full_name: + label: "کمپنی نام (مکمل)" + placeholder: "اِینیٹَیک، انکارپورَیٹِڈ" + company_domain: + label: "کمپنی ڈَومَین نام" + placeholder: "initech.com" + colors: + title: "تھیم" + fields: + theme_id: + description: "شروعات کیلئے کیا آپ لائیٹ یا ڈارک رنگ سکیم پسند کریں گے؟ آپ اپنی سائٹ کی جمالیات کو مذید اپنی مرضی کے مطابق ڈھال سکتے ہیں بذریعہ ایڈمن، \"مرضی کے مطابق بنائیں\"۔" + choices: + default: + label: "سادہ لائیٹ" + dark: + label: "سادہ ڈارک" + logos: + title: "لَوگَو" + fields: + logo_url: + label: "بنیادی لَوگَو" + description: "آپ کی سائٹ کے سب سے اوپر بائیں پر لَوگَو کی تصویر۔ وسیع مستطیل شکل کا استعمال کریں۔" + logo_small_url: + label: "کومپیکٹ لَوگَو" + description: "آپ کے لَوگَو کا ایک کومپیکٹ ورژن، جو نیچے سکرول کرنے پر آپ کی سائٹ کے سب سے اوپر بائیں جانب دکھایا جائے گا۔ ایک مربع شکل کا استعمال کریں۔" + icons: + title: "آئیکن" + fields: + favicon_url: + label: "چھوٹا آئیکن" + description: "آئیکن کی تصویر جو وَیب براؤزروں میں آپ کی ویب سائٹ کی نمائندگی کرنے کیلئے استعمال کی جاتی ہے اور جو چھوٹے سائزوں جیسے کہ 32px by 32px پر اچھی لگتی ہے۔" + apple_touch_icon_url: + label: "بڑا آئیکن" + description: "آئیکن کی تصویر جو جدید ڈِیوائیسِز پر آپ کی سائٹ کی نمائندگی کرنے کیلئے استعمال کی جاتی ہے اور جو بڑے سائزوں پر اچھی لگتی ہے۔ تجویز کردہ سائز کم از کم 144px by 144px ہے۔" + homepage: + description: "ہم آپ کے ہَوم پَیج پر تازہ ترین ٹاپک پیش کرنے کو تجویز کرتے ہیں، لیکن اگر آپ چاہیں تو ہَوم پَیج پر زُمرہ جات (ٹاپکس کے گروپوں) کو بھی دکھا سکتے ہیں۔" + title: "ہَوم پَیج" + fields: + homepage_style: + choices: + latest: + label: "تازہ ترین ٹاپک" + categories: + label: "زُمرَہ جات" + emoji: + title: "اِیمَوجی" + description: "آپ اپنی کمیونٹی کیلئے کونسا اِیمَوجی سڑائل پسند کریں گے؟ آپ ایڈمن، مرضی کے مطابق بنائیں، اِیمَوجی کے ذریعہ بعد میں مزید اپنی مرضی کے اِیمَوجی شامل کرسکتے ہیں۔" + invites: + title: "سٹاف مدعو کریں" + description: "آپ تقریباً کام مکمل کر چکے ہیں! چلیے لوگوں کو دعوت دیتے ہیں کہ آپ کے مباحثوں میں دلچسپ ٹاپک اور جوابات شامل کرنے میں آپ کی مدد کریں تاکہ آپ کی کمیونٹی کا آغاز ہو سکے۔" + finished: + title: "آپ کا ڈِسکورس تیار ہے!" + description: | +

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

    +

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

    + search_logs: + graph_title: "سرچ شمار" + joined: "شمولیت اختیار کی" + date: + <<: *datetime_formats activemodel: errors: <<: *errors - activerecord: - errors: - <<: *errors diff --git a/plugins/poll/config/locales/server.ur.yml b/plugins/poll/config/locales/server.ur.yml index 5541bc579f..ed97700726 100644 --- a/plugins/poll/config/locales/server.ur.yml +++ b/plugins/poll/config/locales/server.ur.yml @@ -35,6 +35,7 @@ ur: no_polls_associated_with_this_post: "کوئی پول اس پوسٹ کے ساتھ منسلک نہیں ہیں۔" no_poll_with_this_name: "کوئی پول جس کا نام %{نام} ہو، اس پوسٹ کے ساتھ منسلک نہیں ہے۔" post_is_deleted: "حذف کر دی گی پوست پر کام نہیں کیا جا سکتا۔" + user_cant_post_in_topic: "آپ ووٹ نہیں کر سکتے کیونکہ آپ اِس ٹاپک میں پوسٹ نہیں کر سکتے ہیں۔" topic_must_be_open_to_vote: "ووٹ کرنے کیلیے ٹاپک کا کھلا ہونا ضروری ہے۔" poll_must_be_open_to_vote: "ووٹ کرنے کیلیے پول کا کھلا ہونا ضروری ہے۔" topic_must_be_open_to_toggle_status: "سٹیٹس بدلنے کیلیے ٹاپک کا کھلا ہونا ضروری ہے۔" diff --git a/script/pull_translations.rb b/script/pull_translations.rb index b1be6f3231..003287b1bc 100644 --- a/script/pull_translations.rb +++ b/script/pull_translations.rb @@ -48,9 +48,6 @@ end languages = get_languages.select { |x| x != 'en' }.sort -# 'ur' translations still have invalid interpolation that breaks the build -languages -= ['ur'] - # ensure that all locale files exists. tx doesn't create missing locale files during pull YML_DIRS.each do |dir| YML_FILE_PREFIXES.each do |prefix| From f81af74ad83489148dd5d37ec3cd470376783158 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Tue, 27 Mar 2018 12:25:05 +0200 Subject: [PATCH 024/287] FIX: makes sure category desc is displayed as row title when possible --- .../javascripts/select-kit/components/category-row.js.es6 | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/assets/javascripts/select-kit/components/category-row.js.es6 b/app/assets/javascripts/select-kit/components/category-row.js.es6 index 8d1c8cd3ed..2f52887b98 100644 --- a/app/assets/javascripts/select-kit/components/category-row.js.es6 +++ b/app/assets/javascripts/select-kit/components/category-row.js.es6 @@ -20,6 +20,12 @@ export default SelectKitRowComponent.extend({ return displayCategoryDescription; }, + @computed("description", "category.name") + title(categoryDescription, categoryName) { + if (categoryDescription) return categoryDescription; + return categoryName; + }, + @computed("computedContent.value", "computedContent.name") category(value, name) { if (Ember.isEmpty(value)) { From f6b6ddd73cf936e80daff19dbaff02b706b4e340 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 27 Mar 2018 18:59:21 +0800 Subject: [PATCH 025/287] REFACTOR: Extract group form into a component. --- .../discourse/components/group-form.js.es6 | 102 ++++++++++ .../discourse/controllers/groups-new.js.es6 | 88 -------- .../templates/components/group-form.hbs | 191 ++++++++++++++++++ .../templates/group/manage/profile.hbs | 74 +------ .../discourse/templates/groups/new.hbs | 180 +---------------- .../group-manage-profile-test.js.es6 | 27 +-- .../acceptance/groups-new-test.js.es6 | 2 +- 7 files changed, 313 insertions(+), 351 deletions(-) create mode 100644 app/assets/javascripts/discourse/components/group-form.js.es6 create mode 100644 app/assets/javascripts/discourse/templates/components/group-form.hbs diff --git a/app/assets/javascripts/discourse/components/group-form.js.es6 b/app/assets/javascripts/discourse/components/group-form.js.es6 new file mode 100644 index 0000000000..f0a9073b95 --- /dev/null +++ b/app/assets/javascripts/discourse/components/group-form.js.es6 @@ -0,0 +1,102 @@ +import computed from 'ember-addons/ember-computed-decorators'; +import User from "discourse/models/user"; +import InputValidation from 'discourse/models/input-validation'; +import debounce from 'discourse/lib/debounce'; + +export default Ember.Component.extend({ + disableSave: null, + + aliasLevelOptions: [ + { name: I18n.t("groups.alias_levels.nobody"), value: 0 }, + { name: I18n.t("groups.alias_levels.mods_and_admins"), value: 2 }, + { name: I18n.t("groups.alias_levels.members_mods_and_admins"), value: 3 }, + { name: I18n.t("groups.alias_levels.everyone"), value: 99 } + ], + + visibilityLevelOptions: [ + { name: I18n.t("groups.visibility_levels.public"), value: 0 }, + { name: I18n.t("groups.visibility_levels.members"), value: 1 }, + { name: I18n.t("groups.visibility_levels.staff"), value: 2 }, + { name: I18n.t("groups.visibility_levels.owners"), value: 3 } + ], + + @computed('model.visibility_level', 'model.public_admission') + disableMembershipRequestSetting(visibility_level, publicAdmission) { + visibility_level = parseInt(visibility_level); + return (visibility_level !== 0) || publicAdmission; + }, + + @computed('model.visibility_level', 'model.allow_membership_requests') + disablePublicSetting(visibility_level, allowMembershipRequests) { + visibility_level = parseInt(visibility_level); + return (visibility_level !== 0) || allowMembershipRequests; + }, + + @computed('basicNameValidation', 'uniqueNameValidation') + nameValidation(basicNameValidation, uniqueNameValidation) { + return uniqueNameValidation ? uniqueNameValidation : basicNameValidation; + }, + + @computed('model.name') + basicNameValidation(name) { + if (name === undefined) { + return this._failedInputValidation(); + }; + + if (name === "") { + this.set('uniqueNameValidation', null); + return this._failedInputValidation(I18n.t('groups.new.name.blank')); + } + + if (name.length < this.siteSettings.min_username_length) { + return this._failedInputValidation(I18n.t('groups.new.name.too_short')); + } + + if (name.length > this.siteSettings.max_username_length) { + return this._failedInputValidation(I18n.t('groups.new.name.too_long')); + } + + this.checkGroupName(); + + return this._failedInputValidation(I18n.t('groups.new.name.checking')); + }, + + checkGroupName: debounce(function() { + User.checkUsername(this.get('model.name')).then(response => { + const validationName = 'uniqueNameValidation'; + + if (response.available) { + this.set(validationName, InputValidation.create({ + ok: true, + reason: I18n.t('groups.new.name.available') + })); + + this.set('disableSave', false); + } else { + let reason; + + if (response.errors) { + reason = response.errors.join(' '); + } else { + reason = I18n.t('groups.new.name.not_available'); + } + + this.set(validationName, this._failedInputValidation(reason)); + } + }); + }, 500), + + _failedInputValidation(reason) { + this.set('disableSave', true); + + const options = { failed: true }; + if (reason) options.reason = reason; + return InputValidation.create(options); + }, + + actions: { + save() { + this.sendAction("save"); + }, + } +}); diff --git a/app/assets/javascripts/discourse/controllers/groups-new.js.es6 b/app/assets/javascripts/discourse/controllers/groups-new.js.es6 index 00714cd8f2..2b829a5eb8 100644 --- a/app/assets/javascripts/discourse/controllers/groups-new.js.es6 +++ b/app/assets/javascripts/discourse/controllers/groups-new.js.es6 @@ -1,94 +1,6 @@ import { popupAjaxError } from 'discourse/lib/ajax-error'; -import computed from 'ember-addons/ember-computed-decorators'; -import User from "discourse/models/user"; -import InputValidation from 'discourse/models/input-validation'; -import debounce from 'discourse/lib/debounce'; export default Ember.Controller.extend({ - disableSave: null, - - aliasLevelOptions: [ - { name: I18n.t("groups.alias_levels.nobody"), value: 0 }, - { name: I18n.t("groups.alias_levels.mods_and_admins"), value: 2 }, - { name: I18n.t("groups.alias_levels.members_mods_and_admins"), value: 3 }, - { name: I18n.t("groups.alias_levels.everyone"), value: 99 } - ], - - visibilityLevelOptions: [ - { name: I18n.t("groups.visibility_levels.public"), value: 0 }, - { name: I18n.t("groups.visibility_levels.members"), value: 1 }, - { name: I18n.t("groups.visibility_levels.staff"), value: 2 }, - { name: I18n.t("groups.visibility_levels.owners"), value: 3 } - ], - - @computed('model.visibility_level', 'model.public_admission') - disableMembershipRequestSetting(visibility_level, publicAdmission) { - visibility_level = parseInt(visibility_level); - return (visibility_level !== 0) || publicAdmission; - }, - - @computed('basicNameValidation', 'uniqueNameValidation') - nameValidation(basicNameValidation, uniqueNameValidation) { - return uniqueNameValidation ? uniqueNameValidation : basicNameValidation; - }, - - @computed('model.name') - basicNameValidation(name) { - if (name === undefined) { - return this._failedInputValidation(); - }; - - if (name === "") { - this.set('uniqueNameValidation', null); - return this._failedInputValidation(I18n.t('groups.new.name.blank')); - } - - if (name.length < this.siteSettings.min_username_length) { - return this._failedInputValidation(I18n.t('groups.new.name.too_short')); - } - - if (name.length > this.siteSettings.max_username_length) { - return this._failedInputValidation(I18n.t('groups.new.name.too_long')); - } - - this.checkGroupName(); - - return this._failedInputValidation(I18n.t('groups.new.name.checking')); - }, - - checkGroupName: debounce(function() { - User.checkUsername(this.get('model.name')).then(response => { - const validationName = 'uniqueNameValidation'; - - if (response.available) { - this.set(validationName, InputValidation.create({ - ok: true, - reason: I18n.t('groups.new.name.available') - })); - - this.set('disableSave', false); - } else { - let reason; - - if (response.errors) { - reason = response.errors.join(' '); - } else { - reason = I18n.t('groups.new.name.not_available'); - } - - this.set(validationName, this._failedInputValidation(reason)); - } - }); - }, 500), - - _failedInputValidation(reason) { - this.set('disableSave', true); - - const options = { failed: true }; - if (reason) options.reason = reason; - return InputValidation.create(options); - }, - actions: { save() { this.set('disableSave', true); diff --git a/app/assets/javascripts/discourse/templates/components/group-form.hbs b/app/assets/javascripts/discourse/templates/components/group-form.hbs new file mode 100644 index 0000000000..465ddc3538 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/group-form.hbs @@ -0,0 +1,191 @@ +
    + {{#if this.currentUser.admin}} +
    + + + {{text-field name="name" + class="input-xxlarge group-form-name" + value=model.name + placeholderKey="groups.name_placeholder"}} + + {{input-tip validation=nameValidation}} +
    + {{/if}} + +
    + + + {{text-field name='full_name' + class="input-xxlarge group-form-full-name" + value=model.full_name}} +
    + + {{#if this.currentUser.admin}} +
    + + + {{input value=model.title name="title" class="input-xxlarge"}} +
    + {{/if}} + +
    + + {{d-editor value=model.bio_raw class="group-form-bio"}} +
    + + {{#if manageMembership}} +
    + + + {{user-selector usernames=model.ownerUsernames + placeholderKey="groups.selector_placeholder" + id="owner-selector"}} +
    + +
    + + + {{user-selector usernames=model.usernames + placeholderKey="groups.selector_placeholder" + id="member-selector"}} +
    + +
    + + + {{combo-box name="alias" + valueAttribute="value" + value=model.visibility_level + content=visibilityLevelOptions + castInteger=true}} +
    + {{/if}} + +
    + {{#if currentUser.admin}} + + {{/if}} + + + + + + + + {{#if model.allow_membership_requests}} +
    + + + {{expanding-text-area name="membership-request-template" + class='group-form-membership-request-template' + value=model.membership_request_template}} +
    + {{/if}} +
    + + {{#if currentUser.admin}} +
    + + + {{combo-box name="alias" + valueAttribute="value" + value=model.mentionable_level + content=aliasLevelOptions}} +
    + +
    + + + {{combo-box name="alias" + valueAttribute="value" + value=model.messageable_level + content=aliasLevelOptions}} +
    + +
    + + + {{notifications-button i18nPrefix='groups.notifications' + value=model.default_notification_level}} +
    + +
    + + + {{list-setting name="automatic_membership" settingValue=model.emailDomains}} + + +
    + +
    + + + {{combo-box name="grant_trust_level" + valueAttribute="value" + value=model.grant_trust_level + content=trustLevelOptions}} +
    + + {{#if siteSettings.email_in}} +
    + + + {{text-field name="incoming_email" + class="input-xxlarge" + value=model.incoming_email + placeholderKey="admin.groups.incoming_email_placeholder"}} + + {{plugin-outlet name="group-email-in" args=(hash model=model)}} +
    + {{/if}} + {{/if}} + +
    + {{group-flair-inputs model=model}} +
    + + {{plugin-outlet name="group-edit" args=(hash group=model)}} + +
    + {{d-button action="save" + disabled=disableSave + class='btn btn-primary' + label='groups.new.create'}} + + {{#link-to "groups"}} + {{i18n 'cancel'}} + {{/link-to}} +
    +
    diff --git a/app/assets/javascripts/discourse/templates/group/manage/profile.hbs b/app/assets/javascripts/discourse/templates/group/manage/profile.hbs index d90ae631b6..816ec0ec4f 100644 --- a/app/assets/javascripts/discourse/templates/group/manage/profile.hbs +++ b/app/assets/javascripts/discourse/templates/group/manage/profile.hbs @@ -1,71 +1,3 @@ -
    - {{#if this.currentUser.admin}} -
    - - {{input type='text' name='name' value=model.name class='group-manage-name'}} -
    - {{/if}} - -
    - - {{input type='text' name='full_name' value=model.full_name class='group-manage-full-name'}} -
    - -
    - - {{d-editor value=model.bio_raw class="group-manage-bio"}} -
    - -
    - {{group-flair-inputs model=model}} -
    - -
    - -
    - -
    - -
    - -
    - -
    - - {{#if model.allow_membership_requests}} -
    - - - {{expanding-text-area name="membership-request-template" - value=model.membership_request_template - class="group-manage-membership-request-template"}} -
    - {{/if}} - - {{plugin-outlet name="group-manage" args=(hash group=model)}} - - {{d-button action="save" class="btn-primary" disabled=saving label="save"}} - {{savingText}} -
    +{{group-form model=model + save="save" + manageMembership=false}} diff --git a/app/assets/javascripts/discourse/templates/groups/new.hbs b/app/assets/javascripts/discourse/templates/groups/new.hbs index 51bca6eb78..7a7b430c54 100644 --- a/app/assets/javascripts/discourse/templates/groups/new.hbs +++ b/app/assets/javascripts/discourse/templates/groups/new.hbs @@ -1,183 +1,5 @@ {{#d-section pageClass="groups-new"}}

    {{i18n "groups.new.title"}}

    -
    -
    - - - {{text-field name="name" - class="input-xxlarge" - value=model.name - placeholderKey="groups.name_placeholder"}} - - {{input-tip validation=nameValidation}} -
    - -
    - - - {{text-field name='full_name' - class="input-xxlarge group-manage-full-name" - value=model.full_name}} -
    - -
    - - - {{input value=model.title name="title" class="input-xxlarge"}} -
    - -
    - - {{d-editor value=model.bio_raw}} -
    - -
    - - - {{user-selector usernames=model.ownerUsernames - placeholderKey="groups.selector_placeholder" - id="owner-selector"}} -
    - -
    - - - {{user-selector usernames=model.usernames - placeholderKey="groups.selector_placeholder" - id="member-selector"}} -
    - -
    - - - {{combo-box name="alias" - valueAttribute="value" - value=model.visibility_level - content=visibilityLevelOptions - castInteger=true}} -
    - -
    - - - - - - - - - {{#if model.allow_membership_requests}} -
    - - - {{expanding-text-area name="membership-request-template" - value=model.membership_request_template}} -
    - {{/if}} -
    - -
    - - - {{combo-box name="alias" - valueAttribute="value" - value=model.mentionable_level - content=aliasLevelOptions}} -
    - -
    - - - {{combo-box name="alias" - valueAttribute="value" - value=model.messageable_level - content=aliasLevelOptions}} -
    - -
    - - - {{notifications-button i18nPrefix='groups.notifications' - value=model.default_notification_level}} -
    - -
    - - - {{list-setting name="automatic_membership" settingValue=model.emailDomains}} - - -
    - -
    - - - {{combo-box name="grant_trust_level" - valueAttribute="value" - value=model.grant_trust_level - content=trustLevelOptions}} -
    - - {{#if siteSettings.email_in}} -
    - - - {{text-field name="incoming_email" - class="input-xxlarge" - value=model.incoming_email - placeholderKey="admin.groups.incoming_email_placeholder"}} - - {{plugin-outlet name="group-email-in" args=(hash model=model)}} -
    - {{/if}} - -
    - {{group-flair-inputs model=model}} -
    - - {{plugin-outlet name="group-edit" args=(hash group=model)}} - -
    - {{d-button action="save" - disabled=disableSave - class='btn btn-primary' - label='groups.new.create'}} - - {{#link-to "groups"}} - {{i18n 'cancel'}} - {{/link-to}} -
    -
    + {{group-form model=model save="save"}} {{/d-section}} diff --git a/test/javascripts/acceptance/group-manage-profile-test.js.es6 b/test/javascripts/acceptance/group-manage-profile-test.js.es6 index fd801b4aed..849272937d 100644 --- a/test/javascripts/acceptance/group-manage-profile-test.js.es6 +++ b/test/javascripts/acceptance/group-manage-profile-test.js.es6 @@ -10,44 +10,47 @@ QUnit.test("Editing group", assert => { andThen(() => { assert.ok(find('.group-flair-inputs').length === 1, 'it should display avatar flair inputs'); - assert.ok(find('.group-manage-bio').length === 1, 'it should display group bio input'); - assert.ok(find('.group-manage-name').length === 1, 'it should display group name input'); - assert.ok(find('.group-manage-full-name').length === 1, 'it should display group full name input'); + assert.ok(find('.group-form-bio').length === 1, 'it should display group bio input'); + assert.ok(find('.group-form-name').length === 1, 'it should display group name input'); + assert.ok(find('.group-form-full-name').length === 1, 'it should display group full name input'); assert.ok( - find('.group-manage-public-admission').length === 1, + find('.group-form-public-admission').length === 1, 'it should display group public admission input' ); assert.ok( - find('.group-manage-public-exit').length === 1, + find('.group-form-public-exit').length === 1, 'it should display group public exit input' ); - assert.ok(find('.group-manage-allow-membership-requests').length === 1, 'it should display group allow_membership_requets input'); + assert.ok( + find('.group-form-allow-membership-requests').length === 1, + 'it should display group allow_membership_request input' + ); assert.ok( - find('.group-manage-allow-membership-requests[disabled]').length === 1, + find('.group-form-allow-membership-requests[disabled]').length === 1, 'it should disable group allow_membership_request input' ); }); - click('.group-manage-public-admission'); - click('.group-manage-allow-membership-requests'); + click('.group-form-public-admission'); + click('.group-form-allow-membership-requests'); andThen(() => { assert.ok( - find('.group-manage-public-admission[disabled]').length === 1, + find('.group-form-public-admission[disabled]').length === 1, 'it should disable group public admission input' ); assert.ok( - find('.group-manage-public-exit[disabled]').length === 0, + find('.group-form-public-exit[disabled]').length === 0, 'it should not disable group public exit input' ); assert.equal( - find('.group-manage-membership-request-template').length, 1, + find('.group-form-membership-request-template').length, 1, 'it should display the membership request template field' ); }); diff --git a/test/javascripts/acceptance/groups-new-test.js.es6 b/test/javascripts/acceptance/groups-new-test.js.es6 index 9a57899bb7..472ce77c07 100644 --- a/test/javascripts/acceptance/groups-new-test.js.es6 +++ b/test/javascripts/acceptance/groups-new-test.js.es6 @@ -61,7 +61,7 @@ QUnit.test("Creating a new group", assert => { ); }); - click(".groups-new-public-admission"); + click(".group-form-public-admission"); andThen(() => { assert.equal( From 757dd1032d151e8ccb4f64faeca53e8fdf3e8f64 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 27 Mar 2018 19:28:38 +0800 Subject: [PATCH 026/287] Incorrect save label for group form. --- .../javascripts/discourse/templates/components/group-form.hbs | 2 +- .../javascripts/discourse/templates/group/manage/profile.hbs | 1 + app/assets/javascripts/discourse/templates/groups/new.hbs | 4 +++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/templates/components/group-form.hbs b/app/assets/javascripts/discourse/templates/components/group-form.hbs index 465ddc3538..c6c82911bc 100644 --- a/app/assets/javascripts/discourse/templates/components/group-form.hbs +++ b/app/assets/javascripts/discourse/templates/components/group-form.hbs @@ -182,7 +182,7 @@ {{d-button action="save" disabled=disableSave class='btn btn-primary' - label='groups.new.create'}} + label=saveLabel}} {{#link-to "groups"}} {{i18n 'cancel'}} diff --git a/app/assets/javascripts/discourse/templates/group/manage/profile.hbs b/app/assets/javascripts/discourse/templates/group/manage/profile.hbs index 816ec0ec4f..e83d163ced 100644 --- a/app/assets/javascripts/discourse/templates/group/manage/profile.hbs +++ b/app/assets/javascripts/discourse/templates/group/manage/profile.hbs @@ -1,3 +1,4 @@ {{group-form model=model + saveLabel="save" save="save" manageMembership=false}} diff --git a/app/assets/javascripts/discourse/templates/groups/new.hbs b/app/assets/javascripts/discourse/templates/groups/new.hbs index 7a7b430c54..b947fe1c12 100644 --- a/app/assets/javascripts/discourse/templates/groups/new.hbs +++ b/app/assets/javascripts/discourse/templates/groups/new.hbs @@ -1,5 +1,7 @@ {{#d-section pageClass="groups-new"}}

    {{i18n "groups.new.title"}}

    - {{group-form model=model save="save"}} + {{group-form model=model + saveLabel="groups.new.create" + save="save"}} {{/d-section}} From b06104a1da8d174f4111781abf86e6eaf5b93156 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 27 Mar 2018 19:30:53 +0800 Subject: [PATCH 027/287] Missing trust level options on group form. --- .../javascripts/discourse/components/group-form.js.es6 | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/assets/javascripts/discourse/components/group-form.js.es6 b/app/assets/javascripts/discourse/components/group-form.js.es6 index f0a9073b95..3ef9de1759 100644 --- a/app/assets/javascripts/discourse/components/group-form.js.es6 +++ b/app/assets/javascripts/discourse/components/group-form.js.es6 @@ -20,6 +20,11 @@ export default Ember.Component.extend({ { name: I18n.t("groups.visibility_levels.owners"), value: 3 } ], + trustLevelOptions: [ + { name: I18n.t("groups.trust_levels.none"), value: 0 }, + { name: 1, value: 1 }, { name: 2, value: 2 }, { name: 3, value: 3 }, { name: 4, value: 4 } + ], + @computed('model.visibility_level', 'model.public_admission') disableMembershipRequestSetting(visibility_level, publicAdmission) { visibility_level = parseInt(visibility_level); From 94deb482ae627e28dc91e4cb0f87d3573065c785 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 27 Mar 2018 19:31:53 +0800 Subject: [PATCH 028/287] Don't show cancel button on edit page. --- .../discourse/templates/components/group-form.hbs | 4 +--- app/assets/javascripts/discourse/templates/groups/new.hbs | 7 ++++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/discourse/templates/components/group-form.hbs b/app/assets/javascripts/discourse/templates/components/group-form.hbs index c6c82911bc..d8a6570af1 100644 --- a/app/assets/javascripts/discourse/templates/components/group-form.hbs +++ b/app/assets/javascripts/discourse/templates/components/group-form.hbs @@ -184,8 +184,6 @@ class='btn btn-primary' label=saveLabel}} - {{#link-to "groups"}} - {{i18n 'cancel'}} - {{/link-to}} + {{yield}}
    diff --git a/app/assets/javascripts/discourse/templates/groups/new.hbs b/app/assets/javascripts/discourse/templates/groups/new.hbs index b947fe1c12..629cb72854 100644 --- a/app/assets/javascripts/discourse/templates/groups/new.hbs +++ b/app/assets/javascripts/discourse/templates/groups/new.hbs @@ -1,7 +1,12 @@ {{#d-section pageClass="groups-new"}}

    {{i18n "groups.new.title"}}

    - {{group-form model=model + {{#group-form model=model saveLabel="groups.new.create" save="save"}} + + {{#link-to "groups"}} + {{i18n 'cancel'}} + {{/link-to}} + {{/group-form}} {{/d-section}} From 518f7ba91b5b87896afa090b280dcddc93aa6dc6 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Tue, 27 Mar 2018 14:00:08 +0530 Subject: [PATCH 029/287] FIX: show private message topic count on admin dashboard reports --- app/models/admin_dashboard_data.rb | 1 + app/models/report.rb | 10 ++++++++-- app/models/topic.rb | 6 ++++++ config/locales/server.en.yml | 4 ++++ spec/models/report_spec.rb | 2 +- 5 files changed, 20 insertions(+), 3 deletions(-) diff --git a/app/models/admin_dashboard_data.rb b/app/models/admin_dashboard_data.rb index 8ba294d424..797e3176eb 100644 --- a/app/models/admin_dashboard_data.rb +++ b/app/models/admin_dashboard_data.rb @@ -21,6 +21,7 @@ class AdminDashboardData PRIVATE_MESSAGE_REPORTS ||= [ 'user_to_user_private_messages', + 'user_to_user_private_messages_with_replies', 'system_private_messages', 'notify_moderators_private_messages', 'notify_user_private_messages', diff --git a/app/models/report.rb b/app/models/report.rb index 04f27453a0..0a2871febf 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -202,14 +202,20 @@ class Report # Private messages counts: def self.private_messages_report(report, topic_subtype) - basic_report_about report, Post, :private_messages_count_per_day, report.start_date, report.end_date, topic_subtype - add_counts report, Post.private_posts.with_topic_subtype(topic_subtype), 'posts.created_at' + basic_report_about report, Topic, :private_message_topics_count_per_day, report.start_date, report.end_date, topic_subtype + add_counts report, Topic.private_messages.with_subtype(topic_subtype), 'topics.created_at' end def self.report_user_to_user_private_messages(report) private_messages_report report, TopicSubtype.user_to_user end + def self.report_user_to_user_private_messages_with_replies(report) + topic_subtype = TopicSubtype.user_to_user + basic_report_about report, Post, :private_messages_count_per_day, report.start_date, report.end_date, topic_subtype + add_counts report, Post.private_posts.with_topic_subtype(topic_subtype), 'posts.created_at' + end + def self.report_system_private_messages(report) private_messages_report report, TopicSubtype.system_message end diff --git a/app/models/topic.rb b/app/models/topic.rb index a9169ad515..3ab042196a 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -191,6 +191,8 @@ class Topic < ActiveRecord::Base where("topics.category_id IS NULL OR topics.category_id IN (SELECT id FROM categories WHERE #{condition[0]})", condition[1]) } + scope :with_subtype, ->(subtype) { where('topics.subtype = ?', subtype) } + attr_accessor :ignore_category_auto_close attr_accessor :skip_callbacks @@ -1307,6 +1309,10 @@ SQL MiniSuffix.domain(URI.parse(URI.encode(self.featured_link)).hostname) end + def self.private_message_topics_count_per_day(start_date, end_date, topic_subtype) + private_messages.with_subtype(topic_subtype).where('topics.created_at >= ? AND topics.created_at <= ?', start_date, end_date).group('date(topics.created_at)').order('date(topics.created_at)').count + end + private def update_category_topic_count_by(num) diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 761d2591c7..f00e044a33 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -873,6 +873,10 @@ en: title: "User-to-User" xaxis: "Day" yaxis: "Number of messages" + user_to_user_private_messages_with_replies: + title: "User-to-User (with replies)" + xaxis: "Day" + yaxis: "Number of messages" system_private_messages: title: "System" xaxis: "Day" diff --git a/spec/models/report_spec.rb b/spec/models/report_spec.rb index a7e5fc8bbb..0d0d8201b0 100644 --- a/spec/models/report_spec.rb +++ b/spec/models/report_spec.rb @@ -166,7 +166,7 @@ describe Report do end describe 'private messages' do - let(:report) { Report.find('user_to_user_private_messages') } + let(:report) { Report.find('user_to_user_private_messages_with_replies') } it 'topic report).to not include private messages' do Fabricate(:private_message_topic, created_at: 1.hour.ago) From ff9d7a9bfbee42aed6fb28a86c21ac8dddcb4c48 Mon Sep 17 00:00:00 2001 From: Vinoth Kannan Date: Tue, 27 Mar 2018 17:22:07 +0530 Subject: [PATCH 030/287] FIX: authComplete query param should carry-forward to login page --- app/controllers/application_controller.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index f2a7b47501..89719d67d7 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -641,6 +641,8 @@ class ApplicationController < ActionController::Base # save original URL in a session so we can redirect after login session[:destination_url] = destination_url redirect_to path('/session/sso') + elsif params[:authComplete].present? + redirect_to path("/login?authComplete=true") else # save original URL in a cookie (javascript redirects after login in this case) cookies[:destination_url] = destination_url From 15aa7122277ca038df75f1af2a11c50674ca6cad Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Tue, 27 Mar 2018 14:11:05 +0200 Subject: [PATCH 031/287] fix category-row regressions --- .../javascripts/select-kit/components/category-row.js.es6 | 4 +++- test/javascripts/components/category-chooser-test.js.es6 | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/select-kit/components/category-row.js.es6 b/app/assets/javascripts/select-kit/components/category-row.js.es6 index 2f52887b98..74f146e51c 100644 --- a/app/assets/javascripts/select-kit/components/category-row.js.es6 +++ b/app/assets/javascripts/select-kit/components/category-row.js.es6 @@ -82,6 +82,8 @@ export default SelectKitRowComponent.extend({ @computed("category.description") description(description) { - return `${description.substr(0, 200)}${description.length > 200 ? '…' : ''}`; + if (description) { + return `${description.substr(0, 200)}${description.length > 200 ? '…' : ''}`; + } } }); diff --git a/test/javascripts/components/category-chooser-test.js.es6 b/test/javascripts/components/category-chooser-test.js.es6 index bb129c872b..ad94f675b0 100644 --- a/test/javascripts/components/category-chooser-test.js.es6 +++ b/test/javascripts/components/category-chooser-test.js.es6 @@ -35,9 +35,9 @@ componentTest('with scopedCategoryId', { this.get('subject').expand(); andThen(() => { - assert.equal(this.get('subject').rowByIndex(0).title(), 'feature'); + assert.equal(this.get('subject').rowByIndex(0).title(), 'Discussion about features or potential features of Discourse: how they work, why they work, etc.'); assert.equal(this.get('subject').rowByIndex(0).value(), 2); - assert.equal(this.get('subject').rowByIndex(1).title(), 'spec'); + assert.equal(this.get('subject').rowByIndex(1).title(), 'My idea here is to have mini specs for features we would like built but have no bandwidth to build'); assert.equal(this.get('subject').rowByIndex(1).value(), 26); assert.equal(this.get('subject').rows().length, 2); }); From 62edf3c40105ed8481abe1c04b8181064a727c57 Mon Sep 17 00:00:00 2001 From: Vinoth Kannan Date: Tue, 27 Mar 2018 18:04:40 +0530 Subject: [PATCH 032/287] Add spec test for authComplete param carry-forward --- spec/requests/application_controller_spec.rb | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 spec/requests/application_controller_spec.rb diff --git a/spec/requests/application_controller_spec.rb b/spec/requests/application_controller_spec.rb new file mode 100644 index 0000000000..2896d6e22e --- /dev/null +++ b/spec/requests/application_controller_spec.rb @@ -0,0 +1,17 @@ +require 'rails_helper' + +RSpec.describe ApplicationController do + describe '#redirect_to_login_if_required' do + let(:admin) { Fabricate(:admin) } + + before do + admin # to skip welcome wizard at home page `/` + SiteSetting.login_required = true + end + + it "should carry-forward authComplete param to login page redirect" do + get "/?authComplete=true" + expect(response).to redirect_to('/login?authComplete=true') + end + end +end From 0187423c68d1b0967d76f11a050304d7a13a76fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Tue, 27 Mar 2018 17:57:53 +0200 Subject: [PATCH 033/287] FIX: discobot certificate description wasn't escaped --- .../lib/discourse_narrative_bot/base.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/base.rb b/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/base.rb index f2bbc9d3b1..8d2ac628c5 100644 --- a/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/base.rb +++ b/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/base.rb @@ -119,10 +119,12 @@ module DiscourseNarrativeBot date: Time.zone.now.strftime('%b %d %Y'), format: :svg } - options.merge!(type: type) if type + src = Discourse.base_url + DiscourseNarrativeBot::Engine.routes.url_helpers.certificate_path(options) - "#{I18n.t("#{self.class::I18N_KEY}.certificate.alt")}" + alt = CGI.escapeHTML(I18n.t("#{self.class::I18N_KEY}.certificate.alt")) + + "#{alt}" end protected From ed4d7ae1b9fba92a3f020982d54744f48e2b945e Mon Sep 17 00:00:00 2001 From: Michael Brown Date: Tue, 27 Mar 2018 12:00:29 -0400 Subject: [PATCH 034/287] FIX: discobot fails when max_emojis_in_title=0 (#5710) * If discobot is enabled but max_emojis_in_title==0, try to strip emoji from the title when creating a new post --- .../discourse_narrative_bot/new_user_narrative.rb | 7 ++++++- plugins/discourse-narrative-bot/spec/user_spec.rb | 15 +++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/new_user_narrative.rb b/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/new_user_narrative.rb index 9bd4819b5f..26ab1437d6 100644 --- a/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/new_user_narrative.rb +++ b/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/new_user_narrative.rb @@ -182,8 +182,13 @@ module DiscourseNarrativeBot #{instance_eval(&@next_instructions)} RAW + title = I18n.t("#{I18N_KEY}.hello.title", title: SiteSetting.title) + if SiteSetting.max_emojis_in_title == 0 + title = title.gsub(/:([\w\-+]+(?::t\d)?):/, '').strip + end + opts = { - title: I18n.t("#{I18N_KEY}.hello.title", title: SiteSetting.title), + title: title, target_usernames: @user.username, archetype: Archetype.private_message, subtype: TopicSubtype.system_message, diff --git a/plugins/discourse-narrative-bot/spec/user_spec.rb b/plugins/discourse-narrative-bot/spec/user_spec.rb index 86dde81499..d0dba54a7e 100644 --- a/plugins/discourse-narrative-bot/spec/user_spec.rb +++ b/plugins/discourse-narrative-bot/spec/user_spec.rb @@ -32,6 +32,21 @@ describe User do end end + context 'with title emoji disabled' do + before do + SiteSetting.disable_discourse_narrative_bot_welcome_post = false + SiteSetting.max_emojis_in_title = 0 + end + + it 'initiates the bot' do + expect { user }.to change { Topic.count }.by(1) + + expect(Topic.last.title).to eq(I18n.t( + 'discourse_narrative_bot.new_user_narrative.hello.title' + ).gsub(/:robot:/, '').strip) + end + end + context 'enabled' do before do SiteSetting.disable_discourse_narrative_bot_welcome_post = false From b01a4c0ada0bbb84f7e84614dd03e3bce8e1c972 Mon Sep 17 00:00:00 2001 From: Michael Brown Date: Tue, 27 Mar 2018 12:11:17 -0400 Subject: [PATCH 035/287] lint: fix whitespace --- .../lib/discourse_narrative_bot/base.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/base.rb b/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/base.rb index 8d2ac628c5..9b685d9b38 100644 --- a/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/base.rb +++ b/plugins/discourse-narrative-bot/lib/discourse_narrative_bot/base.rb @@ -120,10 +120,10 @@ module DiscourseNarrativeBot format: :svg } options.merge!(type: type) if type - + src = Discourse.base_url + DiscourseNarrativeBot::Engine.routes.url_helpers.certificate_path(options) alt = CGI.escapeHTML(I18n.t("#{self.class::I18N_KEY}.certificate.alt")) - + "#{alt}" end From fcd352e0895c6020285ad46cb61a103d04c8a1ab Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Tue, 27 Mar 2018 18:28:37 +0200 Subject: [PATCH 036/287] FIX: Try fixing unparsable email addresses The mail gem returns `UnstructuredField` when it fails to parse email addresses, but the `Receiver` always expects an `AddressList`. --- lib/email/receiver.rb | 11 +++++++++++ spec/components/email/receiver_spec.rb | 8 ++++++++ spec/fixtures/emails/unparsable_email_addresses.eml | 7 +++++++ 3 files changed, 26 insertions(+) create mode 100644 spec/fixtures/emails/unparsable_email_addresses.eml diff --git a/lib/email/receiver.rb b/lib/email/receiver.rb index abf42f61e8..09c1d2540a 100644 --- a/lib/email/receiver.rb +++ b/lib/email/receiver.rb @@ -64,6 +64,7 @@ module Email DistributedMutex.synchronize(@message_id) do begin return if IncomingEmail.exists?(message_id: @message_id) + ensure_valid_address_lists @from_email, @from_display_name = parse_from_field(@mail) @incoming_email = create_incoming_email process_internal @@ -77,6 +78,16 @@ module Email end end + def ensure_valid_address_lists + [:to, :cc, :bcc].each do |field| + addresses = @mail[field] + + if addresses&.errors.present? + @mail[field] = addresses.to_s.scan(/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i) + end + end + end + def is_blacklisted? return false if SiteSetting.ignore_by_title.blank? Regexp.new(SiteSetting.ignore_by_title, Regexp::IGNORECASE) =~ @mail.subject diff --git a/spec/components/email/receiver_spec.rb b/spec/components/email/receiver_spec.rb index 4c3a562f92..c8ba5ee0ef 100644 --- a/spec/components/email/receiver_spec.rb +++ b/spec/components/email/receiver_spec.rb @@ -886,4 +886,12 @@ describe Email::Receiver do end end end + + it "tries to fix unparsable email addresses in To, CC and BBC headers" do + expect { process(:unparsable_email_addresses) }.to raise_error(Email::Receiver::BadDestinationAddress) + + email = IncomingEmail.last + expect(email.to_addresses).to eq("foo@bar.com") + expect(email.cc_addresses).to eq("bob@example.com;carol@example.com") + end end diff --git a/spec/fixtures/emails/unparsable_email_addresses.eml b/spec/fixtures/emails/unparsable_email_addresses.eml new file mode 100644 index 0000000000..c94956d055 --- /dev/null +++ b/spec/fixtures/emails/unparsable_email_addresses.eml @@ -0,0 +1,7 @@ +Date: 27 Mar 2018 11:51:04 +0200 +From: alice@example.com +To: foo@bar.com. +CC: bob@example.com., carol@example.com. +Subject: Email addresses ending with dot + +Lorem ipsum From 4d12ff2e8a4c6e6d571e13a115f20cbf5b4e0e7d Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Tue, 27 Mar 2018 13:44:14 -0400 Subject: [PATCH 037/287] when writing cache, remove elements from the user agents list. also return a message and content type when blocking a crawler. --- app/models/web_crawler_request.rb | 6 +++--- lib/middleware/request_tracker.rb | 2 +- spec/components/middleware/request_tracker_spec.rb | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/models/web_crawler_request.rb b/app/models/web_crawler_request.rb index 3362259905..5e418d1b1c 100644 --- a/app/models/web_crawler_request.rb +++ b/app/models/web_crawler_request.rb @@ -30,9 +30,9 @@ class WebCrawlerRequest < ActiveRecord::Base self.last_flush = Time.now.utc date = date.to_date + ua_list_key = user_agent_list_key(date) - $redis.smembers(user_agent_list_key(date)).each do |user_agent, _| - + while user_agent = $redis.spop(ua_list_key) val = get_and_reset(redis_key(user_agent, date)) next if val == 0 @@ -57,7 +57,7 @@ class WebCrawlerRequest < ActiveRecord::Base $redis.del redis_key(user_agent, date) end - $redis.del list_key + $redis.del(list_key) end protected diff --git a/lib/middleware/request_tracker.rb b/lib/middleware/request_tracker.rb index d22fbfb06d..e7ffb8869a 100644 --- a/lib/middleware/request_tracker.rb +++ b/lib/middleware/request_tracker.rb @@ -167,7 +167,7 @@ class Middleware::RequestTracker if block_crawler(request) log_request = false - result = [403, {}, []] + result = [403, { 'Content-Type' => 'text/plain' }, ['Crawler is not allowed']] return result end diff --git a/spec/components/middleware/request_tracker_spec.rb b/spec/components/middleware/request_tracker_spec.rb index 57f5782121..4faa9c094b 100644 --- a/spec/components/middleware/request_tracker_spec.rb +++ b/spec/components/middleware/request_tracker_spec.rb @@ -291,7 +291,7 @@ describe Middleware::RequestTracker do def expect_blocked_response(status, _, response) expect(status).to eq(403) - expect(response).to be_blank + expect(response).to eq(['Crawler is not allowed']) end it "applies whitelisted_crawler_user_agents correctly" do From df345d80f9f5ce9eb2a4ace9b2ac6ce588d4abe2 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Tue, 27 Mar 2018 13:53:47 -0400 Subject: [PATCH 038/287] fix wrong case --- app/assets/javascripts/admin/models/report.js.es6 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/admin/models/report.js.es6 b/app/assets/javascripts/admin/models/report.js.es6 index 14e059297e..4905bc8a99 100644 --- a/app/assets/javascripts/admin/models/report.js.es6 +++ b/app/assets/javascripts/admin/models/report.js.es6 @@ -137,11 +137,11 @@ const Report = Discourse.Model.extend({ @computed('data') sortedData(data) { - return this.get('xaxisIsDate') ? data.toArray().reverse() : data.toArray(); + return this.get('xAxisIsDate') ? data.toArray().reverse() : data.toArray(); }, @computed('data') - xaxisIsDate() { + xAxisIsDate() { if (!this.data[0]) return false; return this.data && moment(this.data[0].x, 'YYYY-MM-DD').isValid(); } From 3fab5267be3733f8139a719e2903ea6956b490d7 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Tue, 27 Mar 2018 14:10:39 -0400 Subject: [PATCH 039/287] fix web crawler stats sorted in reverse --- app/assets/javascripts/admin/models/report.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/admin/models/report.js.es6 b/app/assets/javascripts/admin/models/report.js.es6 index 4905bc8a99..86281929ae 100644 --- a/app/assets/javascripts/admin/models/report.js.es6 +++ b/app/assets/javascripts/admin/models/report.js.es6 @@ -143,7 +143,7 @@ const Report = Discourse.Model.extend({ @computed('data') xAxisIsDate() { if (!this.data[0]) return false; - return this.data && moment(this.data[0].x, 'YYYY-MM-DD').isValid(); + return this.data && this.data[0].x.match(/\d{4}-\d{1,2}-\d{1,2}/); } }); From 2bd44bbf1368ac920d69701636fcaa5f9498dbc4 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Tue, 27 Mar 2018 15:11:48 -0400 Subject: [PATCH 040/287] WebCrawlerRequest.clear_cache needs to clear user agent list too --- app/models/web_crawler_request.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/web_crawler_request.rb b/app/models/web_crawler_request.rb index 5e418d1b1c..edcad4c9ad 100644 --- a/app/models/web_crawler_request.rb +++ b/app/models/web_crawler_request.rb @@ -51,13 +51,13 @@ class WebCrawlerRequest < ActiveRecord::Base return end - list_key = user_agent_list_key(date) + ua_list_key = user_agent_list_key(date) - $redis.smembers(list_key).each do |user_agent, _| + while user_agent = $redis.spop(ua_list_key) $redis.del redis_key(user_agent, date) end - $redis.del(list_key) + $redis.del(ua_list_key) end protected From eb714d8ae31afb9bceb5c3b77006715e820340c8 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Tue, 27 Mar 2018 15:12:39 -0400 Subject: [PATCH 041/287] FIX: application request count keys not expiring in redis --- app/models/concerns/cached_counting.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/models/concerns/cached_counting.rb b/app/models/concerns/cached_counting.rb index 8f629df70e..867c8fb250 100644 --- a/app/models/concerns/cached_counting.rb +++ b/app/models/concerns/cached_counting.rb @@ -50,7 +50,9 @@ module CachedCounting # for concurrent calls without double counting def get_and_reset(key) namespaced_key = $redis.namespace_key(key) - $redis.without_namespace.eval(GET_AND_RESET, keys: [namespaced_key]).to_i + val = $redis.without_namespace.eval(GET_AND_RESET, keys: [namespaced_key]).to_i + $redis.expire(key, 259200) # SET removes expiry, so set it again + val end def request_id(query_params, retries = 0) From 976d6b290cb66110b5af80c58ef113e297aad0d1 Mon Sep 17 00:00:00 2001 From: Michael Brown Date: Tue, 27 Mar 2018 15:20:22 -0400 Subject: [PATCH 042/287] FIX: CDN_URL hostname should be in GlobalSetting.hostnames --- app/models/global_setting.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/models/global_setting.rb b/app/models/global_setting.rb index 3b747df9a8..f0fb74b5a3 100644 --- a/app/models/global_setting.rb +++ b/app/models/global_setting.rb @@ -117,6 +117,8 @@ class GlobalSetting hostnames = [ hostname ] hostnames << backup_hostname if backup_hostname.present? + hostnames << URI.parse(cdn_url).host if cdn_url.present? + hash["host_names"] = hostnames hash["database"] = db_name From 05dc1f65abb394457ac8c045a43f83268c50bc7b Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Tue, 27 Mar 2018 16:01:58 -0400 Subject: [PATCH 043/287] UX: Editing a shared draft was confusing in the composer Now when you edit a shared draft it looks like creating one, where the destination category id appears in the dropdown. --- .../components/composer-action-title.js.es6 | 12 +++- .../components/shared-draft-controls.js.es6 | 5 +- .../discourse/controllers/composer.js.es6 | 2 +- .../discourse/controllers/topic.js.es6 | 17 ++++- .../discourse/models/composer.js.es6 | 64 ++++++++++++------- .../javascripts/discourse/models/topic.js.es6 | 8 +++ .../discourse/templates/composer.hbs | 1 - config/locales/client.en.yml | 1 + 8 files changed, 77 insertions(+), 33 deletions(-) diff --git a/app/assets/javascripts/discourse/components/composer-action-title.js.es6 b/app/assets/javascripts/discourse/components/composer-action-title.js.es6 index e867e6ab78..3f9483595f 100644 --- a/app/assets/javascripts/discourse/components/composer-action-title.js.es6 +++ b/app/assets/javascripts/discourse/components/composer-action-title.js.es6 @@ -1,11 +1,19 @@ import { default as computed } from 'ember-addons/ember-computed-decorators'; -import { PRIVATE_MESSAGE, CREATE_TOPIC, CREATE_SHARED_DRAFT, REPLY, EDIT } from "discourse/models/composer"; +import { + PRIVATE_MESSAGE, + CREATE_TOPIC, + CREATE_SHARED_DRAFT, + REPLY, + EDIT, + EDIT_SHARED_DRAFT +} from "discourse/models/composer"; import { iconHTML } from 'discourse-common/lib/icon-library'; const TITLES = { [PRIVATE_MESSAGE]: 'topic.private_message', [CREATE_TOPIC]: 'topic.create_long', - [CREATE_SHARED_DRAFT]: 'composer.create_shared_draft' + [CREATE_SHARED_DRAFT]: 'composer.create_shared_draft', + [EDIT_SHARED_DRAFT]: 'composer.edit_shared_draft' }; export default Ember.Component.extend({ diff --git a/app/assets/javascripts/discourse/components/shared-draft-controls.js.es6 b/app/assets/javascripts/discourse/components/shared-draft-controls.js.es6 index a9f35cb8ab..18e6aea527 100644 --- a/app/assets/javascripts/discourse/components/shared-draft-controls.js.es6 +++ b/app/assets/javascripts/discourse/components/shared-draft-controls.js.es6 @@ -13,10 +13,7 @@ export default Ember.Component.extend({ actions: { updateDestinationCategory(category) { - ajax(`/t/${this.get('topic.id')}/shared-draft`, { - method: 'PUT', - data: { category_id: category.get('id') } - }); + return this.get('topic').updateDestinationCategory(category.get('id')); }, publish() { diff --git a/app/assets/javascripts/discourse/controllers/composer.js.es6 b/app/assets/javascripts/discourse/controllers/composer.js.es6 index 9d5b9e7a18..863836c1ba 100644 --- a/app/assets/javascripts/discourse/controllers/composer.js.es6 +++ b/app/assets/javascripts/discourse/controllers/composer.js.es6 @@ -519,7 +519,7 @@ export default Ember.Controller.extend({ if (result.responseJson.action === "create_post" || this.get('replyAsNewTopicDraft') || this.get('replyAsNewPrivateMessageDraft')) { this.destroyDraft(); } - if (this.get('model.action') === 'edit') { + if (this.get('model.editingPost')) { this.appEvents.trigger('post-stream:refresh', { id: parseInt(result.responseJson.id) }); if (result.responseJson.post.post_number === 1) { this.appEvents.trigger('header:update-topic', composer.get('topic')); diff --git a/app/assets/javascripts/discourse/controllers/topic.js.es6 b/app/assets/javascripts/discourse/controllers/topic.js.es6 index 5887bed927..5e1fc79272 100644 --- a/app/assets/javascripts/discourse/controllers/topic.js.es6 +++ b/app/assets/javascripts/discourse/controllers/topic.js.es6 @@ -409,16 +409,29 @@ export default Ember.Controller.extend(BufferedContent, { } const composer = this.get("composer"); + let topic = this.get('model'); const composerModel = composer.get("model"); + let editingFirst = composerModel && (post.get('firstPost') || composerModel.get('editingFirstPost')); + + let editingSharedDraft = false; + let draftsCategoryId = this.get('site.shared_drafts_category_id'); + if (draftsCategoryId && draftsCategoryId === topic.get('category.id')) { + editingSharedDraft = post.get('firstPost'); + } + const opts = { post, - action: Composer.EDIT, + action: editingSharedDraft ? Composer.EDIT_SHARED_DRAFT : Composer.EDIT, draftKey: post.get("topic.draft_key"), draftSequence: post.get("topic.draft_sequence") }; + if (editingSharedDraft) { + opts.destinationCategoryId = topic.get('destination_category_id'); + } + // Cancel and reopen the composer for the first post - if (composerModel && (post.get('firstPost') || composerModel.get('editingFirstPost'))) { + if (editingFirst) { composer.cancelComposer().then(() => composer.open(opts)); } else { composer.open(opts); diff --git a/app/assets/javascripts/discourse/models/composer.js.es6 b/app/assets/javascripts/discourse/models/composer.js.es6 index 2726e0fc2d..52411269e6 100644 --- a/app/assets/javascripts/discourse/models/composer.js.es6 +++ b/app/assets/javascripts/discourse/models/composer.js.es6 @@ -10,6 +10,7 @@ import { escapeExpression, tinyAvatar } from 'discourse/lib/utilities'; export const CREATE_TOPIC = 'createTopic', CREATE_SHARED_DRAFT = 'createSharedDraft', + EDIT_SHARED_DRAFT = 'editSharedDraft', PRIVATE_MESSAGE = 'privateMessage', NEW_PRIVATE_MESSAGE_KEY = 'new_private_message', REPLY = 'reply', @@ -17,6 +18,10 @@ export const REPLY_AS_NEW_TOPIC_KEY = "reply_as_new_topic", REPLY_AS_NEW_PRIVATE_MESSAGE_KEY = "reply_as_new_private_message"; +function isEdit(action) { + return action === EDIT || action === EDIT_SHARED_DRAFT; +} + const CLOSED = 'closed', SAVING = 'saving', OPEN = 'open', @@ -52,11 +57,13 @@ const SAVE_LABELS = { [REPLY]: 'composer.reply', [CREATE_TOPIC]: 'composer.create_topic', [PRIVATE_MESSAGE]: 'composer.create_pm', - [CREATE_SHARED_DRAFT]: 'composer.create_shared_draft' + [CREATE_SHARED_DRAFT]: 'composer.create_shared_draft', + [EDIT_SHARED_DRAFT]: 'composer.save_edit' }; const SAVE_ICONS = { [EDIT]: 'pencil', + [EDIT_SHARED_DRAFT]: 'clipboard', [REPLY]: 'reply', [CREATE_TOPIC]: 'plus', [PRIVATE_MESSAGE]: 'envelope', @@ -116,13 +123,14 @@ const Composer = RestModel.extend({ topicFirstPost: Em.computed.or('creatingTopic', 'editingFirstPost'), - editingPost: Em.computed.equal('action', EDIT), + @computed('action') + editingPost: isEdit, + replyingToTopic: Em.computed.equal('action', REPLY), viewOpen: Em.computed.equal('composeState', OPEN), viewDraft: Em.computed.equal('composeState', DRAFT), - composeStateChanged: function() { var oldOpen = this.get('composerOpened'); @@ -212,7 +220,7 @@ const Composer = RestModel.extend({ if (!this.site.mobileView) { const originalUserName = post.get('reply_to_user.username'); const originalUserAvatar = post.get('reply_to_user.avatar_template'); - if (originalUserName && originalUserAvatar && action === EDIT) { + if (originalUserName && originalUserAvatar && isEdit(action)) { options.originalUser = { username: originalUserName, avatar: tinyAvatar(originalUserAvatar) @@ -471,11 +479,11 @@ const Composer = RestModel.extend({ const composer = this; if (!replyBlank && - ((opts.reply || opts.action === EDIT) && this.get('replyDirty'))) { + ((opts.reply || isEdit(opts.action)) && this.get('replyDirty'))) { return; } - if (opts.action === REPLY && this.get('action') === EDIT) this.set('reply', ''); + if (opts.action === REPLY && isEdit(this.get('action'))) this.set('reply', ''); if (!opts.draftKey) throw 'draft key is required'; if (opts.draftSequence === null) throw 'draft sequence is required'; @@ -527,11 +535,15 @@ const Composer = RestModel.extend({ } // If we are editing a post, load it. - if (opts.action === EDIT && opts.post) { + if (isEdit(opts.action) && opts.post) { const topicProps = this.serialize(_edit_topic_serializer); topicProps.loading = true; + // When editing a shared draft, use its category + if (opts.action === EDIT_SHARED_DRAFT && opts.destinationCategoryId) { + topicProps.categoryId = opts.destinationCategoryId; + } this.setProperties(topicProps); this.store.find('post', opts.post.get('id')).then(function(post) { @@ -591,21 +603,25 @@ const Composer = RestModel.extend({ // When you edit a post editPost(opts) { - const post = this.get('post'), - oldCooked = post.get('cooked'), - self = this; + let post = this.get('post'); + let oldCooked = post.get('cooked'); + let promise = Ember.RSVP.resolve(); - let promise; - - // Update the title if we've changed it, otherwise consider it a - // successful resolved promise + // Update the topic if we're editing the first post if (this.get('title') && post.get('post_number') === 1 && this.get('topic.details.can_edit')) { const topicProps = this.getProperties(Object.keys(_edit_topic_serializer)); - promise = Topic.update(this.get('topic'), topicProps); - } else { - promise = Ember.RSVP.resolve(); + + let topic = this.get('topic'); + + // If we're editing a shared draft, keep the original category + if (this.get('action') === EDIT_SHARED_DRAFT) { + let destinationCategoryId = topicProps.categoryId; + promise = promise.then(() => topic.updateDestinationCategory(destinationCategoryId)); + topicProps.categoryId = topic.get('category.id'); + } + promise = promise.then(() => Topic.update(topic, topicProps)); } const props = { @@ -617,18 +633,18 @@ const Composer = RestModel.extend({ this.set('composeState', SAVING); - var rollback = throwAjaxError(function(){ + let rollback = throwAjaxError(() => { post.set('cooked', oldCooked); - self.set('composeState', OPEN); + this.set('composeState', OPEN); }); - return promise.then(function() { + return promise.then(() => { // rest model only sets props after it is saved post.set("cooked", props.cooked); - return post.save(props).then(function(result) { - self.clearState(); + return post.save(props).then(result => { + this.clearState(); return result; - }).catch(function(error) { + }).catch(error => { throw error; }); }).catch(rollback); @@ -867,6 +883,8 @@ Composer.reopenClass({ // The actions the composer can take CREATE_TOPIC, + CREATE_SHARED_DRAFT, + EDIT_SHARED_DRAFT, PRIVATE_MESSAGE, REPLY, EDIT, diff --git a/app/assets/javascripts/discourse/models/topic.js.es6 b/app/assets/javascripts/discourse/models/topic.js.es6 index 7ca45bb022..6df5cbe8c2 100644 --- a/app/assets/javascripts/discourse/models/topic.js.es6 +++ b/app/assets/javascripts/discourse/models/topic.js.es6 @@ -488,6 +488,14 @@ const Topic = RestModel.extend({ }).catch(popupAjaxError); }, + updateDestinationCategory(categoryId) { + this.set('destination_category_id', categoryId); + return ajax(`/t/${this.get('id')}/shared-draft`, { + method: 'PUT', + data: { category_id: categoryId } + }); + }, + convertTopic(type) { return ajax(`/t/${this.get('id')}/convert-topic/${type}`, {type: 'PUT'}).then(() => { window.location.reload(); diff --git a/app/assets/javascripts/discourse/templates/composer.hbs b/app/assets/javascripts/discourse/templates/composer.hbs index e07a6eddf8..fdbad6c3e4 100644 --- a/app/assets/javascripts/discourse/templates/composer.hbs +++ b/app/assets/javascripts/discourse/templates/composer.hbs @@ -9,7 +9,6 @@ {{composer-messages composer=model messageCount=messageCount addLinkLookup="addLinkLookup"}} - {{#if model.viewOpen}}
    diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index b42d7e5bc0..d3091c7a5e 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -1314,6 +1314,7 @@ en: create_pm: "Message" create_whisper: "Whisper" create_shared_draft: "Create Shared Draft" + edit_shared_draft: "Edit Shared Draft" title: "Or press Ctrl+Enter" users_placeholder: "Add a user" From ddefc29c4e2b5aa98776a19437ecc327629c00dc Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Tue, 27 Mar 2018 17:19:38 -0400 Subject: [PATCH 044/287] FIX: Lint error --- .../discourse/components/shared-draft-controls.js.es6 | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/javascripts/discourse/components/shared-draft-controls.js.es6 b/app/assets/javascripts/discourse/components/shared-draft-controls.js.es6 index 18e6aea527..0fb51515c3 100644 --- a/app/assets/javascripts/discourse/components/shared-draft-controls.js.es6 +++ b/app/assets/javascripts/discourse/components/shared-draft-controls.js.es6 @@ -1,5 +1,4 @@ import computed from 'ember-addons/ember-computed-decorators'; -import { ajax } from 'discourse/lib/ajax'; export default Ember.Component.extend({ tagName: '', From 90c0198a57939777591dda7255de5773f543fd25 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Tue, 27 Mar 2018 17:26:53 -0400 Subject: [PATCH 045/287] FIX: watched word counts always show as 0 --- .../controllers/admin-watched-words-action.js.es6 | 14 ++++++++++---- .../admin/templates/watched-words-action.hbs | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/admin/controllers/admin-watched-words-action.js.es6 b/app/assets/javascripts/admin/controllers/admin-watched-words-action.js.es6 index bd11f15fcf..9e899c4296 100644 --- a/app/assets/javascripts/admin/controllers/admin-watched-words-action.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-watched-words-action.js.es6 @@ -10,11 +10,11 @@ export default Ember.Controller.extend({ return (this.get('adminWatchedWords.model') || []).findBy('nameKey', actionName); }, - @computed('adminWatchedWords.model', 'actionNameKey') - filteredContent() { - if (!this.get('actionNameKey')) { return []; } + @computed('actionNameKey', 'adminWatchedWords.model') + filteredContent(actionNameKey) { + if (!actionNameKey) { return []; } - const a = this.findAction(this.get('actionNameKey')); + const a = this.findAction(actionNameKey); return a ? a.words : []; }, @@ -23,6 +23,12 @@ export default Ember.Controller.extend({ return I18n.t('admin.watched_words.action_descriptions.' + actionNameKey); }, + @computed('actionNameKey', 'adminWatchedWords.model') + wordCount(actionNameKey) { + const a = this.findAction(actionNameKey); + return a ? a.words.length : 0; + }, + actions: { recordAdded(arg) { const a = this.findAction(this.get('actionNameKey')); diff --git a/app/assets/javascripts/admin/templates/watched-words-action.hbs b/app/assets/javascripts/admin/templates/watched-words-action.hbs index d58f1a9430..ac0bc4175b 100644 --- a/app/assets/javascripts/admin/templates/watched-words-action.hbs +++ b/app/assets/javascripts/admin/templates/watched-words-action.hbs @@ -22,6 +22,6 @@
    {{admin-watched-word word=word action="recordRemoved"}}
    {{/each}} {{else}} - {{i18n 'admin.watched_words.word_count' count=model.words.length}} + {{i18n 'admin.watched_words.word_count' count=wordCount}} {{/if}}
    From 08d68e846c48ab09549975ad7708728f533b152b Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 28 Mar 2018 09:03:35 +0800 Subject: [PATCH 046/287] UX: Don't default title to label text for buttons. --- .../javascripts/discourse/components/d-button.js.es6 | 9 +++++++-- app/assets/javascripts/discourse/widgets/button.js.es6 | 9 ++++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/discourse/components/d-button.js.es6 b/app/assets/javascripts/discourse/components/d-button.js.es6 index c56e1b6b34..13e36edc71 100644 --- a/app/assets/javascripts/discourse/components/d-button.js.es6 +++ b/app/assets/javascripts/discourse/components/d-button.js.es6 @@ -6,7 +6,7 @@ export default Ember.Component.extend({ tagName: 'button', classNameBindings: [':btn', 'noText', 'btnType'], - attributeBindings: ['disabled', 'translatedTitle:title', 'tabindex'], + attributeBindings: ['disabled', 'translatedTitle:title', 'ariaLabel:aria-label', 'tabindex'], btnIcon: Ember.computed.notEmpty('icon'), @@ -23,7 +23,7 @@ export default Ember.Component.extend({ @computed("title") translatedTitle(title) { - return title ? I18n.t(title) : this.get('translatedLabel'); + if (title) return I18n.t(title); }, @computed("label") @@ -31,6 +31,11 @@ export default Ember.Component.extend({ if (label) return I18n.t(label); }, + @computed("translatedTitle", "translatedLabel") + ariaLabel(translatedTitle, translatedLabel) { + return translatedTitle ? translatedTitle : translatedLabel; + }, + click() { this.sendAction("action", this.get("actionParam")); return false; diff --git a/app/assets/javascripts/discourse/widgets/button.js.es6 b/app/assets/javascripts/discourse/widgets/button.js.es6 index aa65deae5e..b9b288676a 100644 --- a/app/assets/javascripts/discourse/widgets/button.js.es6 +++ b/app/assets/javascripts/discourse/widgets/button.js.es6 @@ -32,11 +32,14 @@ export const ButtonClass = { let title; if (attrs.title) { title = I18n.t(attrs.title, attrs.titleOptions); - } else if (attrs.label) { - title = I18n.t(attrs.label, attrs.labelOptions); } - const attributes = { "aria-label": title, title }; + let label; + if (attrs.label) { + label = I18n.t(attrs.label, attrs.labelOptions); + } + + const attributes = { "aria-label": label, title }; if (attrs.disabled) { attributes.disabled = "true"; } if (attrs.data) { From 8bc5da57b09d77663c7b6ef81dafeec4b9873f95 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 28 Mar 2018 09:11:50 +0800 Subject: [PATCH 047/287] No need to default `aria-label` to label. --- .../javascripts/discourse/components/d-button.js.es6 | 7 +------ .../javascripts/discourse/widgets/button.js.es6 | 12 ++++-------- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/app/assets/javascripts/discourse/components/d-button.js.es6 b/app/assets/javascripts/discourse/components/d-button.js.es6 index 13e36edc71..464a434bf1 100644 --- a/app/assets/javascripts/discourse/components/d-button.js.es6 +++ b/app/assets/javascripts/discourse/components/d-button.js.es6 @@ -6,7 +6,7 @@ export default Ember.Component.extend({ tagName: 'button', classNameBindings: [':btn', 'noText', 'btnType'], - attributeBindings: ['disabled', 'translatedTitle:title', 'ariaLabel:aria-label', 'tabindex'], + attributeBindings: ['disabled', 'translatedTitle:title', 'translatedTitle:aria-label', 'tabindex'], btnIcon: Ember.computed.notEmpty('icon'), @@ -31,11 +31,6 @@ export default Ember.Component.extend({ if (label) return I18n.t(label); }, - @computed("translatedTitle", "translatedLabel") - ariaLabel(translatedTitle, translatedLabel) { - return translatedTitle ? translatedTitle : translatedLabel; - }, - click() { this.sendAction("action", this.get("actionParam")); return false; diff --git a/app/assets/javascripts/discourse/widgets/button.js.es6 b/app/assets/javascripts/discourse/widgets/button.js.es6 index b9b288676a..1794c0ab85 100644 --- a/app/assets/javascripts/discourse/widgets/button.js.es6 +++ b/app/assets/javascripts/discourse/widgets/button.js.es6 @@ -28,18 +28,14 @@ export const ButtonClass = { buildAttributes() { const attrs = this.attrs; + const attributes = {}; - let title; if (attrs.title) { - title = I18n.t(attrs.title, attrs.titleOptions); + const title = I18n.t(attrs.title, attrs.titleOptions); + attributes["aria-label"] = title; + attributes.title = title; } - let label; - if (attrs.label) { - label = I18n.t(attrs.label, attrs.labelOptions); - } - - const attributes = { "aria-label": label, title }; if (attrs.disabled) { attributes.disabled = "true"; } if (attrs.data) { From baa383b7f16a0c89926a8dd94a14c88483798783 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 28 Mar 2018 09:42:12 +0800 Subject: [PATCH 048/287] Fix the build. --- .../discourse/templates/components/group-form.hbs | 2 +- .../discourse/templates/modal/forgot-password.hbs | 2 +- .../javascripts/acceptance/forgot-password-test.js.es6 | 10 +++++----- test/javascripts/acceptance/groups-new-test.js.es6 | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/discourse/templates/components/group-form.hbs b/app/assets/javascripts/discourse/templates/components/group-form.hbs index d8a6570af1..43eeea9837 100644 --- a/app/assets/javascripts/discourse/templates/components/group-form.hbs +++ b/app/assets/javascripts/discourse/templates/components/group-form.hbs @@ -181,7 +181,7 @@
    {{d-button action="save" disabled=disableSave - class='btn btn-primary' + class='btn btn-primary group-form-save' label=saveLabel}} {{yield}} diff --git a/app/assets/javascripts/discourse/templates/modal/forgot-password.hbs b/app/assets/javascripts/discourse/templates/modal/forgot-password.hbs index dd24392734..f2d00aac48 100644 --- a/app/assets/javascripts/discourse/templates/modal/forgot-password.hbs +++ b/app/assets/javascripts/discourse/templates/modal/forgot-password.hbs @@ -12,7 +12,7 @@ {{d-button action="resetPassword" label="forgot_password.reset" disabled=submitDisabled - class="btn-primary"}} + class="btn-primary forgot-password-reset"}} {{else}} {{d-button class="btn-large btn-primary" label="forgot_password.button_ok" diff --git a/test/javascripts/acceptance/forgot-password-test.js.es6 b/test/javascripts/acceptance/forgot-password-test.js.es6 index 5e21fc8b60..19a0d39c6b 100644 --- a/test/javascripts/acceptance/forgot-password-test.js.es6 +++ b/test/javascripts/acceptance/forgot-password-test.js.es6 @@ -25,14 +25,14 @@ QUnit.test("requesting password reset", assert => { andThen(() => { assert.equal( - find('button[title="Reset Password"]').attr("disabled"), + find('.forgot-password-reset').attr("disabled"), "disabled", 'it should disable the button until the field is filled' ); }); fillIn("#username-or-email", 'someuser'); - click('button[title="Reset Password"]'); + click('.forgot-password-reset'); andThen(() => { assert.equal( @@ -43,7 +43,7 @@ QUnit.test("requesting password reset", assert => { }); fillIn("#username-or-email", 'someuser@gmail.com'); - click('button[title="Reset Password"]'); + click('.forgot-password-reset'); andThen(() => { assert.equal( @@ -59,7 +59,7 @@ QUnit.test("requesting password reset", assert => { userFound = true; }); - click('button[title="Reset Password"]'); + click('.forgot-password-reset'); andThen(() => { assert.notOk(exists(find(".alert-error")), 'it should remove the flash error when succeeding'); @@ -75,7 +75,7 @@ QUnit.test("requesting password reset", assert => { click("header .login-button"); click('#forgot-password-link'); fillIn("#username-or-email", 'someuser@gmail.com'); - click('button[title="Reset Password"]'); + click('.forgot-password-reset'); andThen(() => { assert.equal( diff --git a/test/javascripts/acceptance/groups-new-test.js.es6 b/test/javascripts/acceptance/groups-new-test.js.es6 index 472ce77c07..7d8b3d9f74 100644 --- a/test/javascripts/acceptance/groups-new-test.js.es6 +++ b/test/javascripts/acceptance/groups-new-test.js.es6 @@ -29,7 +29,7 @@ QUnit.test("Creating a new group", assert => { ); assert.ok( - find("button[title='Create']:disabled").length === 1, + find(".group-form-save:disabled").length === 1, 'it should disable the save button' ); }); From 70be8124a3169f75c41e8db232a018627a996dd4 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 28 Mar 2018 11:22:43 +0800 Subject: [PATCH 049/287] SECURITY: Don't expose development route in production. --- app/controllers/session_controller.rb | 18 +++++++++++------- config/routes.rb | 5 ++++- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/app/controllers/session_controller.rb b/app/controllers/session_controller.rb index 2421ebbcae..5d9f8473bc 100644 --- a/app/controllers/session_controller.rb +++ b/app/controllers/session_controller.rb @@ -10,7 +10,7 @@ class SessionController < ApplicationController before_action :check_local_login_allowed, only: %i(create forgot_password email_login) before_action :rate_limit_login, only: %i(create email_login) skip_before_action :redirect_to_login_if_required - skip_before_action :preload_json, :check_xhr, only: %i(sso sso_login become sso_provider destroy email_login) + skip_before_action :preload_json, :check_xhr, only: %i(sso sso_login sso_provider destroy email_login) ACTIVATE_USER_KEY = "activate_user" @@ -75,13 +75,17 @@ class SessionController < ApplicationController # For use in development mode only when login options could be limited or disabled. # NEVER allow this to work in production. - def become - raise Discourse::InvalidAccess.new unless Rails.env.development? - user = User.find_by_username(params[:session_id]) - raise "User #{params[:session_id]} not found" if user.blank? + if Rails.env.development? + skip_before_action :check_xhr, only: [:become] - log_on_user(user) - redirect_to path("/") + def become + raise Discourse::InvalidAccess if Rails.env.production? + user = User.find_by_username(params[:session_id]) + raise "User #{params[:session_id]} not found" if user.blank? + + log_on_user(user) + redirect_to path("/") + end end def sso_login diff --git a/config/routes.rb b/config/routes.rb index 7f49f013e9..241d404f0b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -292,7 +292,10 @@ Discourse::Application.routes.draw do get "extra-locales/:bundle" => "extra_locales#show" resources :session, id: RouteFormat.username, only: [:create, :destroy, :become] do - get 'become' + if Rails.env.development? + get 'become' + end + collection do post "forgot_password" end From 21ae49ab9282422d52cae24d983494d72b3a4105 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 28 Mar 2018 11:31:43 +0800 Subject: [PATCH 050/287] Simplify log in for request specs. --- app/controllers/session_controller.rb | 2 +- config/routes.rb | 2 +- spec/requests/users_controller_spec.rb | 6 +++--- spec/support/integration_helpers.rb | 6 +----- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/app/controllers/session_controller.rb b/app/controllers/session_controller.rb index 5d9f8473bc..29b4e5256f 100644 --- a/app/controllers/session_controller.rb +++ b/app/controllers/session_controller.rb @@ -75,7 +75,7 @@ class SessionController < ApplicationController # For use in development mode only when login options could be limited or disabled. # NEVER allow this to work in production. - if Rails.env.development? + if !Rails.env.production? skip_before_action :check_xhr, only: [:become] def become diff --git a/config/routes.rb b/config/routes.rb index 241d404f0b..8039c8ef62 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -292,7 +292,7 @@ Discourse::Application.routes.draw do get "extra-locales/:bundle" => "extra_locales#show" resources :session, id: RouteFormat.username, only: [:create, :destroy, :become] do - if Rails.env.development? + if !Rails.env.production? get 'become' end diff --git a/spec/requests/users_controller_spec.rb b/spec/requests/users_controller_spec.rb index bd5f961f1f..6f8be029cf 100644 --- a/spec/requests/users_controller_spec.rb +++ b/spec/requests/users_controller_spec.rb @@ -436,7 +436,7 @@ RSpec.describe UsersController do SiteSetting.enable_local_logins = false post "/users/second_factors.json", params: { - password: 'somecomplicatedpassword' + password: 'myawesomepassword' } expect(response.status).to eq(404) @@ -449,7 +449,7 @@ RSpec.describe UsersController do SiteSetting.enable_sso = true post "/users/second_factors.json", params: { - password: 'somecomplicatedpassword' + password: 'myawesomepassword' } expect(response.status).to eq(404) @@ -461,7 +461,7 @@ RSpec.describe UsersController do user.user_second_factor.update!(data: "abcdefghijklmnop") post "/users/second_factors.json", params: { - password: 'somecomplicatedpassword' + password: 'myawesomepassword' } expect(response.status).to eq(200) diff --git a/spec/support/integration_helpers.rb b/spec/support/integration_helpers.rb index ceea993555..94d245caae 100644 --- a/spec/support/integration_helpers.rb +++ b/spec/support/integration_helpers.rb @@ -24,11 +24,7 @@ module IntegrationHelpers end def sign_in(user) - password = 'somecomplicatedpassword' - user.update!(password: password) - Fabricate(:email_token, confirmed: true, user: user) - post "/session.json", params: { login: user.username, password: password } - expect(response).to be_success + get "/session/#{user.username}/become" user end end From 5f4ff4a8c09f7d87ed048049b4862e4b0d37e230 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 28 Mar 2018 12:01:50 +0800 Subject: [PATCH 051/287] Fix failing spec. --- spec/controllers/session_controller_spec.rb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/spec/controllers/session_controller_spec.rb b/spec/controllers/session_controller_spec.rb index 1f980a1fcb..e06d85a523 100644 --- a/spec/controllers/session_controller_spec.rb +++ b/spec/controllers/session_controller_spec.rb @@ -11,10 +11,12 @@ describe SessionController do describe '#become' do let!(:user) { Fabricate(:user) } - it "does not work when not in development mode" do - Rails.env.stubs(:development?).returns(false) + it "does not work when in production mode" do + Rails.env.stubs(:production?).returns(true) get :become, params: { session_id: user.username }, format: :json - expect(response).not_to be_redirect + + expect(response.status).to eq(403) + expect(JSON.parse(response.body)["error_type"]).to eq("invalid_access") expect(session[:current_user_id]).to be_blank end From 347e4eadbcaa217894b0d6789cf955e970d04340 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 28 Mar 2018 12:54:11 +0800 Subject: [PATCH 052/287] Don't retry trying to download a file in test. --- app/jobs/regular/pull_hotlinked_images.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/jobs/regular/pull_hotlinked_images.rb b/app/jobs/regular/pull_hotlinked_images.rb index b920c60590..7de892a84e 100644 --- a/app/jobs/regular/pull_hotlinked_images.rb +++ b/app/jobs/regular/pull_hotlinked_images.rb @@ -24,7 +24,7 @@ module Jobs follow_redirect: true ) rescue - if (retries -= 1) > 0 + if (retries -= 1) > 0 && !Rails.env.test? sleep 1 retry end From 03725c7c8aa4cb2c15270834f1a440dcb7a79217 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Wed, 28 Mar 2018 10:47:20 +0530 Subject: [PATCH 053/287] =?UTF-8?q?FIX:=20add=20reserved=20usernames=20for?= =?UTF-8?q?=20=E2=80=98/u/=E2=80=99=20static=20routes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../javascripts/discourse/initializers/url-redirects.js.es6 | 2 +- config/site_settings.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/initializers/url-redirects.js.es6 b/app/assets/javascripts/discourse/initializers/url-redirects.js.es6 index afe7dbe6bd..9b952941d1 100644 --- a/app/assets/javascripts/discourse/initializers/url-redirects.js.es6 +++ b/app/assets/javascripts/discourse/initializers/url-redirects.js.es6 @@ -21,7 +21,7 @@ export default { } DiscourseURL.rewrite(/^\/u\/([^\/]+)\/?$/, "/u/$1/summary", { - exceptions: ['/u/account-created', '/users/account-created'] + exceptions: ['/u/account-created', '/users/account-created', '/u/password-reset', '/users/password-reset'] }); } }; diff --git a/config/site_settings.yml b/config/site_settings.yml index 3b1b7f00c2..814b281ee5 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -358,7 +358,7 @@ users: max: 60 reserved_usernames: type: list - default: "admin|moderator|administrator|mod|sys|system|community|info|you|name|username|user|nickname|discourse|discourseorg|discourseforum|support|account-created" + default: "admin|moderator|administrator|mod|sys|system|community|info|you|name|username|user|nickname|discourse|discourseorg|discourseforum|support|hp|account-created|password-reset|admin-login|confirm-admin|account-created|activate-account|confirm-email-token|authorize-email" min_password_length: client: true default: 10 From ee69d58a59c824a7a0b1146710930c5861d3ca26 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 28 Mar 2018 14:44:42 +0800 Subject: [PATCH 054/287] FIX: Tests could get stucked in infinite loop if it fails to resolve IP of a hostname. --- app/jobs/regular/pull_hotlinked_images.rb | 15 +++++++++++---- lib/final_destination.rb | 10 +++++----- spec/models/user_avatar_spec.rb | 3 +++ 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/app/jobs/regular/pull_hotlinked_images.rb b/app/jobs/regular/pull_hotlinked_images.rb index 7de892a84e..0bbef3a470 100644 --- a/app/jobs/regular/pull_hotlinked_images.rb +++ b/app/jobs/regular/pull_hotlinked_images.rb @@ -62,7 +62,9 @@ module Jobs if is_valid_image_url(src) begin # have we already downloaded that file? - unless downloaded_images.include?(src) || large_images.include?(src) || broken_images.include?(src) + schemeless_src = remove_scheme(original_src) + + unless downloaded_images.include?(schemeless_src) || large_images.include?(schemeless_src) || broken_images.include?(schemeless_src) if hotlinked = download(src) if File.size(hotlinked.path) <= @max_size filename = File.basename(URI.parse(src).path) @@ -70,17 +72,17 @@ module Jobs upload = UploadCreator.new(hotlinked, filename, origin: src).create_for(post.user_id) if upload.persisted? downloaded_urls[src] = upload.url - downloaded_images[src.sub(/^https?:/i, "")] = upload.id + downloaded_images[remove_scheme(src)] = upload.id has_downloaded_image = true else log(:info, "Failed to pull hotlinked image for post: #{post_id}: #{src} - #{upload.errors.full_messages.join("\n")}") end else - large_images << original_src.sub(/^https?:/i, "") + large_images << remove_scheme(src) has_new_large_image = true end else - broken_images << original_src.sub(/^https?:/i, "") + broken_images << remove_scheme(src) has_new_broken_image = true end end @@ -170,6 +172,11 @@ module Jobs ) end + private + + def remove_scheme(src) + src.sub(/^https?:/i, "") + end end end diff --git a/lib/final_destination.rb b/lib/final_destination.rb index e85aff8c01..3b91788a52 100644 --- a/lib/final_destination.rb +++ b/lib/final_destination.rb @@ -312,11 +312,11 @@ class FinalDestination end def self.lookup_ip(host) - # TODO clean this up in the test suite, cause it is a mess - # if Rails.env == "test" - # STDERR.puts "WARNING FinalDestination.lookup_ip was called with host: #{host}, this is network call that should be mocked" - # end - IPSocket::getaddress(host) + if Rails.env.test? + "0.0.0.0" + else + IPSocket::getaddress(host) + end rescue SocketError nil end diff --git a/spec/models/user_avatar_spec.rb b/spec/models/user_avatar_spec.rb index eb7f723064..75f31ac8db 100644 --- a/spec/models/user_avatar_spec.rb +++ b/spec/models/user_avatar_spec.rb @@ -46,6 +46,9 @@ describe UserAvatar do describe 'when avatar url returns an invalid status code' do it 'should not do anything' do + stub_request(:get, "http://thisfakesomething.something.com/") + .to_return(status: 500, body: "", headers: {}) + url = "http://thisfakesomething.something.com/" UserAvatar.import_url_for_user(url, user) From 70c4630320d9c91f5fda4bb0358db91e8b768898 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Wed, 28 Mar 2018 13:06:57 +0530 Subject: [PATCH 055/287] FEATURE: show sub navigation for selected PM tag --- .../routes/user-private-messages-tags-show.js.es6 | 1 + .../discourse/templates/user/messages.hbs | 15 +++++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/discourse/routes/user-private-messages-tags-show.js.es6 b/app/assets/javascripts/discourse/routes/user-private-messages-tags-show.js.es6 index 6b2fa60e29..3ab2439e86 100644 --- a/app/assets/javascripts/discourse/routes/user-private-messages-tags-show.js.es6 +++ b/app/assets/javascripts/discourse/routes/user-private-messages-tags-show.js.es6 @@ -2,6 +2,7 @@ import createPMRoute from "discourse/routes/build-private-messages-route"; export default createPMRoute('tags', 'private-messages-tags').extend({ model(params) { + this.controllerFor('user-private-messages').set('tagId', params.id); const username = this.modelFor("user").get("username_lower"); return this.store.findFiltered("topicList", { filter: `topics/private-messages-tags/${username}/${params.id}` diff --git a/app/assets/javascripts/discourse/templates/user/messages.hbs b/app/assets/javascripts/discourse/templates/user/messages.hbs index 1a8982ae74..0e53780129 100644 --- a/app/assets/javascripts/discourse/templates/user/messages.hbs +++ b/app/assets/javascripts/discourse/templates/user/messages.hbs @@ -40,10 +40,17 @@ {{#if pmTaggingEnabled}}
  • - {{#link-to 'userPrivateMessages.tags' model}} - {{i18n 'user.messages.tags'}} - {{/link-to}} -
  • + {{#link-to 'userPrivateMessages.tags' model}} + {{i18n 'user.messages.tags'}} + {{/link-to}} + + {{#if tagId}} +
  • + {{#link-to 'userPrivateMessages.tagsShow' tagId}} + {{tagId}} + {{/link-to}} +
  • + {{/if}} {{/if}} {{/mobile-nav}} {{/d-section}} From 334a611e1325df9f8011df619c3a942b8b4f19cb Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Wed, 28 Mar 2018 13:08:47 +0200 Subject: [PATCH 056/287] FIX: adds spacing between category name and text in topic timers --- .../javascripts/discourse/components/topic-timer-info.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/components/topic-timer-info.js.es6 b/app/assets/javascripts/discourse/components/topic-timer-info.js.es6 index 16ea751834..98c9ad1c30 100644 --- a/app/assets/javascripts/discourse/components/topic-timer-info.js.es6 +++ b/app/assets/javascripts/discourse/components/topic-timer-info.js.es6 @@ -54,7 +54,7 @@ export default Ember.Component.extend(bufferedRender({ }, options); } - buffer.push(I18n.t(this._noticeKey(), options)); + buffer.push(`${I18n.t(this._noticeKey(), options)}`); buffer.push(''); // TODO Sam: concerned this can cause a heavy rerender loop From 2c1ede6e5fb45aa87b4929b01c5ad953f543c2ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Wed, 28 Mar 2018 13:12:50 +0200 Subject: [PATCH 057/287] update email_reply_trimmer --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 75c6438657..5378557430 100644 --- a/Gemfile +++ b/Gemfile @@ -59,7 +59,7 @@ gem 'aws-sdk-s3', require: false gem 'excon', require: false gem 'unf', require: false -gem 'email_reply_trimmer', '0.1.10' +gem 'email_reply_trimmer', '0.1.11' # Forked until https://github.com/toy/image_optim/pull/149 is merged gem 'discourse_image_optim', require: 'image_optim' diff --git a/Gemfile.lock b/Gemfile.lock index f3b04f834b..019b39ec03 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -91,7 +91,7 @@ GEM image_size (~> 1.5) in_threads (~> 1.3) progress (~> 3.0, >= 3.0.1) - email_reply_trimmer (0.1.10) + email_reply_trimmer (0.1.11) ember-data-source (2.2.1) ember-source (>= 1.8, < 3.0) ember-handlebars-template (0.7.5) @@ -417,7 +417,7 @@ DEPENDENCIES cppjieba_rb discourse-qunit-rails discourse_image_optim - email_reply_trimmer (= 0.1.10) + email_reply_trimmer (= 0.1.11) ember-handlebars-template (= 0.7.5) ember-rails (= 0.18.5) ember-source (= 2.13.3) From e5dc8ab5c1032909aad5130e02a14dec3e1cf94e Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Wed, 28 Mar 2018 13:19:25 +0200 Subject: [PATCH 058/287] FIX: correctly localizes period chooser row title --- .../components/period-chooser/period-chooser-row.js.es6 | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/select-kit/components/period-chooser/period-chooser-row.js.es6 b/app/assets/javascripts/select-kit/components/period-chooser/period-chooser-row.js.es6 index 0a31c0a7c7..0ab99cd42b 100644 --- a/app/assets/javascripts/select-kit/components/period-chooser/period-chooser-row.js.es6 +++ b/app/assets/javascripts/select-kit/components/period-chooser/period-chooser-row.js.es6 @@ -1,6 +1,12 @@ import DropdownSelectBoxRowComponent from "select-kit/components/dropdown-select-box/dropdown-select-box-row"; +import computed from "ember-addons/ember-computed-decorators"; export default DropdownSelectBoxRowComponent.extend({ layoutName: "select-kit/templates/components/period-chooser/period-chooser-row", - classNames: "period-chooser-row" + classNames: "period-chooser-row", + + @computed("computedContent") + title(computedContent) { + return I18n.t(`filters.top.${computedContent.name || 'this_week'}`).title; + } }); From dc33f2d071b39ecacd7323f28aa42c48f2b0bd5c Mon Sep 17 00:00:00 2001 From: Vinoth Kannan Date: Wed, 28 Mar 2018 17:40:29 +0530 Subject: [PATCH 059/287] Add new web hook serializers --- app/jobs/regular/emit_web_hook_event.rb | 12 ++++++++++++ .../web_hook_category_serializer.rb | 12 ++++++++++++ app/serializers/web_hook_group_serializer.rb | 12 ++++++++++++ spec/fabricators/web_hook_fabricator.rb | 19 +++++++++++++++++++ spec/jobs/emit_web_hook_event_spec.rb | 11 ++++++++--- 5 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 app/serializers/web_hook_category_serializer.rb create mode 100644 app/serializers/web_hook_group_serializer.rb diff --git a/app/jobs/regular/emit_web_hook_event.rb b/app/jobs/regular/emit_web_hook_event.rb index cc80fcebb2..a893b49689 100644 --- a/app/jobs/regular/emit_web_hook_event.rb +++ b/app/jobs/regular/emit_web_hook_event.rb @@ -50,6 +50,18 @@ module Jobs args[:payload] = WebHookUserSerializer.new(user, scope: guardian, root: false).as_json end + def setup_group(args) + group = Group.find(args[:group_id]) + return if group.blank? + args[:payload] = WebHookGroupSerializer.new(group, scope: guardian, root: false).as_json + end + + def setup_category(args) + category = Category.find(args[:category_id]) + return if category.blank? + args[:payload] = WebHookCategorySerializer.new(category, scope: guardian, root: false).as_json + end + def ping_event?(event_type) event_type.to_s == 'ping'.freeze end diff --git a/app/serializers/web_hook_category_serializer.rb b/app/serializers/web_hook_category_serializer.rb new file mode 100644 index 0000000000..02708ad154 --- /dev/null +++ b/app/serializers/web_hook_category_serializer.rb @@ -0,0 +1,12 @@ +class WebHookCategorySerializer < CategorySerializer + + %i{ + can_edit + notification_level + }.each do |attr| + define_method("include_#{attr}?") do + false + end + end + +end diff --git a/app/serializers/web_hook_group_serializer.rb b/app/serializers/web_hook_group_serializer.rb new file mode 100644 index 0000000000..c580451db8 --- /dev/null +++ b/app/serializers/web_hook_group_serializer.rb @@ -0,0 +1,12 @@ +class WebHookGroupSerializer < GroupShowSerializer + + %i{ + is_group_user + is_group_owner + }.each do |attr| + define_method("include_#{attr}?") do + false + end + end + +end diff --git a/spec/fabricators/web_hook_fabricator.rb b/spec/fabricators/web_hook_fabricator.rb index d9354fb59e..937fc0fee6 100644 --- a/spec/fabricators/web_hook_fabricator.rb +++ b/spec/fabricators/web_hook_fabricator.rb @@ -21,6 +21,9 @@ Fabricator(:wildcard_web_hook, from: :web_hook) do wildcard_web_hook true end +Fabricator(:post_web_hook, from: :web_hook) do +end + Fabricator(:topic_web_hook, from: :web_hook) do transient topic_hook: WebHookEventType.find_by(name: 'topic') @@ -36,3 +39,19 @@ Fabricator(:user_web_hook, from: :web_hook) do web_hook.web_hook_event_types = [transients[:user_hook]] end end + +Fabricator(:group_web_hook, from: :web_hook) do + transient group_hook: WebHookEventType.find_by(name: 'group') + + after_build do |web_hook, transients| + web_hook.web_hook_event_types = [transients[:group_hook]] + end +end + +Fabricator(:category_web_hook, from: :web_hook) do + transient category_hook: WebHookEventType.find_by(name: 'category') + + after_build do |web_hook, transients| + web_hook.web_hook_event_types = [transients[:category_hook]] + end +end diff --git a/spec/jobs/emit_web_hook_event_spec.rb b/spec/jobs/emit_web_hook_event_spec.rb index b7e1cca39d..29be1e6925 100644 --- a/spec/jobs/emit_web_hook_event_spec.rb +++ b/spec/jobs/emit_web_hook_event_spec.rb @@ -56,9 +56,14 @@ describe Jobs::EmitWebHookEvent do stub_request(:post, "https://meta.discourse.org/webhook_listener") .to_return(body: 'OK', status: 200) - expect do - subject.execute(web_hook_id: post_hook.id, event_type: 'post', post_id: post.id) - end.to change(WebHookEvent, :count).by(1) + WebHookEventType.all.pluck(:name).each do |name| + web_hook_id = Fabricate("#{name}_web_hook").id + object_id = Fabricate(name).id + + expect do + subject.execute(web_hook_id: web_hook_id, event_type: name, "#{name}_id": object_id) + end.to change(WebHookEvent, :count).by(1) + end end it 'skips silently on missing post' do From b5da0b579667f24b91ad45a64e3ac16d9c8bfead Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 28 Mar 2018 09:14:45 -0400 Subject: [PATCH 060/287] FIX: Missing translation key --- config/locales/client.en.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index d3091c7a5e..b5d2236856 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -3347,6 +3347,7 @@ en: post_unlocked: "post unlocked" check_personal_message: "check personal message" disabled_second_factor: "disable Two Factor Authentication" + topic_published: "topic published" screened_emails: title: "Screened Emails" description: "When someone tries to create a new account, the following email addresses will be checked and the registration will be blocked, or some other action performed." From 9e7d5a3cdfb3720effa771ae7f56c86e71f954c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Wed, 28 Mar 2018 15:51:47 +0200 Subject: [PATCH 061/287] FIX: 'uploads:recover_from_tombstone' rake task wasn't restoring attachments --- lib/tasks/uploads.rake | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/lib/tasks/uploads.rake b/lib/tasks/uploads.rake index 0332f89652..d69f3078a7 100644 --- a/lib/tasks/uploads.rake +++ b/lib/tasks/uploads.rake @@ -409,8 +409,14 @@ def recover_from_tombstone end begin - original_setting = SiteSetting.max_image_size_kb - SiteSetting.max_image_size_kb = 10240 + previous_image_size = SiteSetting.max_image_size_kb + previous_attachment_size = SiteSetting.max_attachment_size_kb + previous_extensions = SiteSetting.authorized_extensions + + SiteSetting.max_image_size_kb = 10 * 1024 + SiteSetting.max_attachment_size_kb = 10 * 1024 + SiteSetting.authorized_extensions = "*" + current_db = RailsMultisite::ConnectionManagement.current_db public_path = Rails.root.join("public") paths = Dir.glob(File.join(public_path, 'uploads', 'tombstone', current_db, '**', '*.*')) @@ -424,9 +430,10 @@ def recover_from_tombstone doc = Nokogiri::HTML::fragment(post.raw) updated = false - doc.css("img[src]").each do |img| - url = img["src"] + image_urls = doc.css("img[src]").map { |img| img["src"] } + attachment_urls = doc.css("a.attachment[href]").map { |a| a["href"] } + (image_urls + attachment_urls).each do |url| next if !url.start_with?("/uploads/") next if Upload.exists?(url: url) @@ -473,7 +480,9 @@ def recover_from_tombstone end end ensure - SiteSetting.max_image_size_kb = original_setting + SiteSetting.max_image_size_kb = previous_image_size + SiteSetting.max_attachment_size_kb = previous_attachment_size + SiteSetting.authorized_extensions = previous_extensions end end From 466f09bbc4ab39efd2fca87bdf399cc16c155975 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Wed, 28 Mar 2018 17:05:42 +0200 Subject: [PATCH 062/287] FIX: remove uneeded and uninformative title on user notifications list --- .../discourse/widgets/notification-item.js.es6 | 6 ++---- config/locales/client.ar.yml | 14 -------------- config/locales/client.bs_BA.yml | 13 ------------- config/locales/client.ca.yml | 13 ------------- config/locales/client.cs.yml | 16 ---------------- config/locales/client.da.yml | 13 ------------- config/locales/client.de.yml | 16 ---------------- config/locales/client.el.yml | 13 ------------- config/locales/client.en.yml | 17 ----------------- config/locales/client.es.yml | 16 ---------------- config/locales/client.et.yml | 15 --------------- config/locales/client.fa_IR.yml | 13 ------------- config/locales/client.fi.yml | 15 --------------- config/locales/client.fr.yml | 16 ---------------- config/locales/client.gl.yml | 13 ------------- config/locales/client.he.yml | 13 ------------- config/locales/client.id.yml | 6 ------ config/locales/client.it.yml | 14 -------------- config/locales/client.ja.yml | 13 ------------- config/locales/client.ko.yml | 13 ------------- config/locales/client.lv.yml | 13 ------------- config/locales/client.nb_NO.yml | 16 ---------------- config/locales/client.nl.yml | 13 ------------- config/locales/client.pl_PL.yml | 14 -------------- config/locales/client.pt.yml | 16 ---------------- config/locales/client.pt_BR.yml | 14 -------------- config/locales/client.ro.yml | 15 --------------- config/locales/client.ru.yml | 14 -------------- config/locales/client.sk.yml | 14 -------------- config/locales/client.sl.yml | 6 ------ config/locales/client.sq.yml | 13 ------------- config/locales/client.sr.yml | 2 -- config/locales/client.sv.yml | 13 ------------- config/locales/client.th.yml | 13 ------------- config/locales/client.tr_TR.yml | 14 -------------- config/locales/client.uk.yml | 2 -- config/locales/client.ur.yml | 16 ---------------- config/locales/client.vi.yml | 13 ------------- config/locales/client.zh_CN.yml | 14 -------------- config/locales/client.zh_TW.yml | 13 ------------- 40 files changed, 2 insertions(+), 514 deletions(-) diff --git a/app/assets/javascripts/discourse/widgets/notification-item.js.es6 b/app/assets/javascripts/discourse/widgets/notification-item.js.es6 index 83f0c368fa..1b5892fff7 100644 --- a/app/assets/javascripts/discourse/widgets/notification-item.js.es6 +++ b/app/assets/javascripts/discourse/widgets/notification-item.js.es6 @@ -101,10 +101,8 @@ createWidget('notification-item', { let { data } = attrs; let infoKey = notName === 'custom' ? data.message : notName; - let title = I18n.t(`notifications.alt.${infoKey}`); - let icon = iconNode(`notification.${infoKey}`, { title }); - let text = emojiUnescape(this.text(notificationType, notName)); + let icon = iconNode(`notification.${infoKey}`); // We can use a `

    ` tag here once other languages have fixed their HTML // translations. @@ -113,7 +111,7 @@ createWidget('notification-item', { let contents = [ icon, html ]; const href = this.url(); - return href ? h('a', { attributes: { href, title, 'data-auto-route': true } }, contents) : contents; + return href ? h('a', { attributes: { href, 'data-auto-route': true } }, contents) : contents; }, click(e) { diff --git a/config/locales/client.ar.yml b/config/locales/client.ar.yml index 8ae149fefa..9ed9689e34 100644 --- a/config/locales/client.ar.yml +++ b/config/locales/client.ar.yml @@ -1301,20 +1301,6 @@ ar: few: "{{count}} رسالة في صندوق رسائل {{group_name}} " many: "{{count}} رسالة في صندوق رسائل {{group_name}} " other: "{{count}} رسالة في صندوق رسائل {{group_name}} " - alt: - mentioned: "أُشير إليك من" - quoted: "أُقتبس كلامك من" - replied: "مجاب" - posted: "منشور من" - edited: "تم تعديل منشورك من" - liked: "أُعجب بمنشورك" - invited_to_topic: "تمت دعوتك لموضوع من " - invitee_accepted: "قبلت دعوتك من" - moved_post: "منشورك نقل بواسطة" - linked: "وضع رابطا لمنشورك" - granted_badge: "تم منح الوسام" - group_message_summary: "رسائل صندوق المجموعة" - topic_reminder: "تذكير" popup: mentioned: 'أشار {{username}} إليك في "{{topic}}" - {{site_title}}' group_mentioned: 'أشار {{username}} إليك في "{{topic}}" - {{site_title}}' diff --git a/config/locales/client.bs_BA.yml b/config/locales/client.bs_BA.yml index d29c809f7c..bfb5c21d47 100644 --- a/config/locales/client.bs_BA.yml +++ b/config/locales/client.bs_BA.yml @@ -996,19 +996,6 @@ bs_BA: empty: "Nema obavještenja." more: "pogledaj starija obaviještenja" total_flagged: "ukupno opomenutih postova" - alt: - mentioned: "Spomenut od" - quoted: "Citiran od" - replied: "Odgovoreno" - posted: "Post od" - edited: "Editujte vaš post" - liked: "Vole vaš post" - invited_to_topic: "Pozvani na temu od" - invitee_accepted: "Pozivnica prihvaćena od" - moved_post: "Vaša objava je premještena od strane " - linked: "Link ka vašoj objavi" - granted_badge: "Bedž odobren" - group_message_summary: "Poruke u grupnom sandučetu" popup: mentioned: '{{username}} vas je spomenuo/la u "{{topic}}" - {{site_title}}' group_mentioned: '{{username}} vas je spomenuo/la u "{{topic}}" - {{site_title}}' diff --git a/config/locales/client.ca.yml b/config/locales/client.ca.yml index b706ad4cd7..15a5fa6ac8 100644 --- a/config/locales/client.ca.yml +++ b/config/locales/client.ca.yml @@ -1088,19 +1088,6 @@ ca: total_flagged: "total publicacions amb bandera" mentioned: "{{username}} {{description}}" group_mentioned: "{{username}} {{description}}" - alt: - mentioned: "Mencionat per" - quoted: "Citat per" - replied: "Respost" - posted: "Publicat per" - edited: "Edita la teva publicació per" - liked: "Els ha agradat la teva entrada" - invited_to_topic: "Ha convidat a un tema de" - invitee_accepted: "Invitació acceptada per" - moved_post: "La teva publicació ha estat moguda per" - linked: "Enllaç a la teva publicació" - granted_badge: "Distintiu atorgat" - group_message_summary: "Missatges a la bústia d'entrada del grup" popup: mentioned: '{{username}} t''ha mencionat a "{{topic}}" - {{site_title}}' group_mentioned: '{{username}} t''ha mencionat a "{{topic}}" - {{site_title}}' diff --git a/config/locales/client.cs.yml b/config/locales/client.cs.yml index 9d9f0ed68b..698c0a0ee7 100644 --- a/config/locales/client.cs.yml +++ b/config/locales/client.cs.yml @@ -1287,22 +1287,6 @@ cs: one: "{{count}} zpráva ve schránce skupiny {{group_name}}" few: "{{count}} zprávy ve schránce skupiny {{group_name}}" other: "{{count}} zpráv ve schránce skupiny {{group_name}}" - alt: - mentioned: "Zmíněno" - quoted: "Citováno" - replied: "Odpověděl" - posted: "Příspěvek od" - edited: "Editovat váš příspěvek od" - liked: "Líbil se tvůj příspěvek" - private_message: "Soukromá zpráva od" - invited_to_private_message: "Pozván k soukromé zprávě od" - invited_to_topic: "Pozván k tématu od" - invitee_accepted: "Pozvánka přijata od" - moved_post: "Tvůj příspěvek přesunul" - linked: "Odkaz na tvůj příspěvek" - granted_badge: "Odznak přidělen" - group_message_summary: "Zprávy ve skupinové schránce" - topic_reminder: "Připomenutí" popup: mentioned: '{{username}} vás zmínil v "{{topic}}" - {{site_title}}' group_mentioned: 'Uživatel {{username}} vás zmínil v "{{topic}}" - {{site_title}}' diff --git a/config/locales/client.da.yml b/config/locales/client.da.yml index 6ab5a3647a..ee523d0fff 100644 --- a/config/locales/client.da.yml +++ b/config/locales/client.da.yml @@ -1082,19 +1082,6 @@ da: empty: "Ingen notifikationer fundet." more: "se ældre notifikationer" total_flagged: "total markerede indlæg" - alt: - mentioned: "Nævnt af" - quoted: "Citeret af" - replied: "Svaret" - posted: "Indlæg af" - edited: "Rediger dit indlæg af" - liked: "Likede dit indlæg" - invited_to_topic: "Inviteret til et indlæg fra" - invitee_accepted: "Invitation accepteret af" - moved_post: "Dit indlæg blev flyttet af" - linked: "Link til dit indlæg" - granted_badge: "Badge tildelt" - group_message_summary: "Besker i gruppens indbakke" popup: mentioned: '{{username}} nævnte dig i "{{topic}}" - {{site_title}}' group_mentioned: '{{username}} nævnte dig i "{{topic}}" - {{site_title}}' diff --git a/config/locales/client.de.yml b/config/locales/client.de.yml index a9733b1606..3dcb812371 100644 --- a/config/locales/client.de.yml +++ b/config/locales/client.de.yml @@ -1280,22 +1280,6 @@ de: group_message_summary: one: "{{count}} Nachricht in deinem {{group_name}} Posteingang" other: "{{count}} Nachrichten in deinem {{group_name}} Posteingang" - alt: - mentioned: "Erwähnt von" - quoted: "Zitiert von" - replied: "Geantwortet" - posted: "Beitrag von" - edited: "Beitrag bearbeitet von" - liked: "Gefällt dein Beitrag" - private_message: "Absender der Nachricht" - invited_to_private_message: "Eingeladen zu einer Nachricht von" - invited_to_topic: "Zu Thema eingeladen von" - invitee_accepted: "Einladung angenommen von" - moved_post: "Dein Beitrag wurde verschoben von" - linked: "Link zu deinem Beitrag" - granted_badge: "Abzeichen erhalten" - group_message_summary: "Nachrichten im Gruppenpostfach" - topic_reminder: "Eine Erinnerung" popup: mentioned: '{{username}} hat dich in "{{topic}}" - {{site_title}} erwähnt' group_mentioned: '{{username}} hat dich in "{{topic}}" - {{site_title}} erwähnt' diff --git a/config/locales/client.el.yml b/config/locales/client.el.yml index 13ef058e4d..d3271ccb7e 100644 --- a/config/locales/client.el.yml +++ b/config/locales/client.el.yml @@ -1167,19 +1167,6 @@ el: group_message_summary: one: "{{count}} μήνυμα στα εισερχόμενα της ομάδας {{group_name}}" other: "{{count}} μηνύματα στα εισερχόμενα της ομάδας {{group_name}} " - alt: - mentioned: "Αναφέρθηκε από" - quoted: "Παράθεση από " - replied: "Απαντήθηκε" - posted: "Ανάρτηση από" - edited: "Επεξεργασία της ανάρτησής σου από" - liked: "Η ανάρτησή σου έχει \"μου αρέσει\"" - invited_to_topic: "Προσκλήθηκε στο νήμα από" - invitee_accepted: "Η πρόσκληση έγινε δεκτή από" - moved_post: "Η ανάρτησή σου μετακινήθηκε από" - linked: "Σύνδεση στην ανάρτησή σου" - granted_badge: "Χορηγήθηκε παράσημο" - group_message_summary: "Μηνύματα στα εισερχόμενα ομάδας" popup: mentioned: '{{username}} σε ανέφερε στο "{{topic}}" - {{site_title}}' group_mentioned: '{{username}} σε ανέφερε στο "{{topic}}" - {{site_title}}' diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index b42d7e5bc0..0c45e20f17 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -1428,23 +1428,6 @@ en: one: "{{count}} message in your {{group_name}} inbox" other: "{{count}} messages in your {{group_name}} inbox" - alt: - mentioned: "Mentioned by" - quoted: "Quoted by" - replied: "Replied" - posted: "Post by" - edited: "Edit your post by" - liked: "Liked your post" - private_message: "Personal message from" - invited_to_private_message: "Invited to a personal message from" - invited_to_topic: "Invited to a topic from" - invitee_accepted: "Invite accepted by" - moved_post: "Your post was moved by" - linked: "Link to your post" - granted_badge: "Badge granted" - group_message_summary: "Messages in group inbox" - topic_reminder: "A reminder" - popup: mentioned: '{{username}} mentioned you in "{{topic}}" - {{site_title}}' group_mentioned: '{{username}} mentioned you in "{{topic}}" - {{site_title}}' diff --git a/config/locales/client.es.yml b/config/locales/client.es.yml index 22a6031ac7..bdfe685602 100644 --- a/config/locales/client.es.yml +++ b/config/locales/client.es.yml @@ -1184,22 +1184,6 @@ es: group_message_summary: one: "{{count}} mensaje en tu bandeja de {{group_name}}" other: "{{count}} mensajes en tu bandeja de {{group_name}} " - alt: - mentioned: "Mencionado por" - quoted: "Citado por" - replied: "Respondido" - posted: "Publicado por" - edited: "Editado tu post por" - liked: "Gustado tu post" - private_message: "Mensaje personal de" - invited_to_private_message: "Invitado a un mensaje personal de" - invited_to_topic: "Invitado a un tema de" - invitee_accepted: "Invitación aceptada por" - moved_post: "Tu post fue eliminado por" - linked: "Enlace a tu post" - granted_badge: "Distintivo concedido" - group_message_summary: "Mensajes en la bandeja del grupo" - topic_reminder: "Un recordatorio" popup: mentioned: '{{username}} te mencionó en "{{topic}}" - {{site_title}}' group_mentioned: '{{username}} te ha mencionado en "{{topic}}" - {{site_title}}' diff --git a/config/locales/client.et.yml b/config/locales/client.et.yml index 3061b09ba0..021e977bc9 100644 --- a/config/locales/client.et.yml +++ b/config/locales/client.et.yml @@ -1145,21 +1145,6 @@ et: linked: "{{username}} {{description}}" topic_reminder: "{{username}} {{description}}" watching_first_post: "New Topic {{description}}" - alt: - mentioned: "Mainis" - quoted: "Tsiteeris" - replied: "Vastas" - posted: "Postitas" - edited: "Muuda oma postitust" - liked: "Sinu postitus meeldis" - private_message: "Privaatsõnum kasutajalt" - invited_to_topic: "Teemasse kutsus" - invitee_accepted: "Kutse võttis vastu" - moved_post: "Sinu postituse liigutas" - linked: "Viide sinu postitusele" - granted_badge: "Märgis antud" - group_message_summary: "Sõnumeid grupi postkastis" - topic_reminder: "Meelespea" popup: mentioned: '{{username}} mainis Sind teemas "{{topic}}" - {{site_title}}' group_mentioned: '{{username}} mainis Sind teemas "{{topic}}" - {{site_title}}' diff --git a/config/locales/client.fa_IR.yml b/config/locales/client.fa_IR.yml index e1e34a2ee7..728ffe2546 100644 --- a/config/locales/client.fa_IR.yml +++ b/config/locales/client.fa_IR.yml @@ -1070,19 +1070,6 @@ fa_IR: empty: "اعلانی پیدا نشد." more: "نمایش اعلان‌های قدیمی‌تر" total_flagged: "همه‌ی نوشته‌های پرچم خورده" - alt: - mentioned: "اشاره شده توسط" - quoted: "نقل‌قول شده توسط" - replied: "پاسخ داده شد" - posted: "نوشته شده توسط" - edited: "نوشته‌تان را ویرایش کنید توسط" - liked: "ارسال شما را پسندید" - invited_to_topic: "دعوت شده به یک موضوع توسط" - invitee_accepted: "دعوت قبول شد توسط" - moved_post: "نوشته‌ی شما منتقل شد توسط" - linked: "پیوند به ارسال شما" - granted_badge: "نشان اعطا شد" - group_message_summary: "پیام‌های صندوق ورودی گروه" popup: mentioned: '{{username}} در "{{topic}}" - {{site_title}} به شما اشاره کرد' group_mentioned: '{{username}} به شما در "{{topic}}" - {{site_title}} اشاره نمود' diff --git a/config/locales/client.fi.yml b/config/locales/client.fi.yml index 0581fc702e..315c531192 100644 --- a/config/locales/client.fi.yml +++ b/config/locales/client.fi.yml @@ -1220,21 +1220,6 @@ fi: group_message_summary: one: "{{count}} viesti ryhmän {{group_name}} saapuneissa" other: "{{count}} viestiä ryhmän {{group_name}} saapuneissa" - alt: - mentioned: "Mainitsija" - quoted: "Lainaaja" - replied: "Vastasi" - posted: "Kirjoittaja" - edited: "Viestieäsi muokkasi" - liked: "Viestistäsi tykkäsi" - private_message: "Yksityisviesti käyttäjältä" - invited_to_topic: "Kutsu ketjuun käyttäjältä" - invitee_accepted: "Kutsun hyväksyi" - moved_post: "Viestisi siirsi" - linked: "Linkki viestiisi" - granted_badge: "Ansiomerkki myönnetty" - group_message_summary: "Viestejä ryhmän saapuneissa viesteissä." - topic_reminder: "Muistutus" popup: mentioned: '{{username}} mainitsi sinut ketjussa "{{topic}}" - {{site_title}}' group_mentioned: '{{username}} mainitsi sinut ketjussa "{{topic}}" - {{site_title}}' diff --git a/config/locales/client.fr.yml b/config/locales/client.fr.yml index 3618d9cd37..1313b5fecb 100644 --- a/config/locales/client.fr.yml +++ b/config/locales/client.fr.yml @@ -1281,22 +1281,6 @@ fr: group_message_summary: one: "{{count}} message dans la boîte de réception de {{group_name}}" other: "{{count}} messages dans la boîte de réception de {{group_name}}" - alt: - mentioned: "Mentionné par" - quoted: "Cité par" - replied: "Répondu" - posted: "Message par" - edited: "Modifier votre message par" - liked: "A aimé votre message" - private_message: "Message direct de" - invited_to_private_message: "Invité à un message direct par" - invited_to_topic: "Invité à un sujet par" - invitee_accepted: "Invitation acceptée par" - moved_post: "Votre message a été déplacé par" - linked: "Lien vers votre message" - granted_badge: "Badge attribué" - group_message_summary: "Messages dans la boite de réception du groupe" - topic_reminder: "Un rappel" popup: mentioned: '{{username}} vous a mentionné dans « {{topic}} » - {{site_title}}' group_mentioned: '{{username}} vous a mentionné dans « {{topic}} » - {{site_title}}' diff --git a/config/locales/client.gl.yml b/config/locales/client.gl.yml index e3b9c50df1..28a5753792 100644 --- a/config/locales/client.gl.yml +++ b/config/locales/client.gl.yml @@ -848,19 +848,6 @@ gl: none: "Non é posíbel cargar as notificacións neste intre" more: "ver notificacións anteriores" total_flagged: "total de publicacións denunciadas" - alt: - mentioned: "Mencionado por" - quoted: "Citado por" - replied: "Respondido" - posted: "Publicado por" - edited: "A túa publicación ediotuna" - liked: "Gustoulles a túa publicación" - invited_to_topic: "Convidado a un tema de" - invitee_accepted: "Convite aceptado por" - moved_post: "A túa publicación foi movida por" - linked: "Ligazón á túa publicación" - granted_badge: "Insignias concedidas" - group_message_summary: "Mensaxes na caixa do grupo" popup: mentioned: '{{username}} mencionoute en "{{topic}}" - {{site_title}}' group_mentioned: '{{username}} mencionoute en "{{topic}}" - {{site_title}}' diff --git a/config/locales/client.he.yml b/config/locales/client.he.yml index 9a9d3197be..d82ba1893e 100644 --- a/config/locales/client.he.yml +++ b/config/locales/client.he.yml @@ -1091,19 +1091,6 @@ he: empty: "לא נמצאו התראות." more: "הצגת התראות ישנות יותר" total_flagged: "סך הכל פוסטים מדוגלים" - alt: - mentioned: "הוזכר על ידי" - quoted: "צוטט על ידי" - replied: "השיב" - posted: "פורסם על ידי" - edited: "ערוך את הפוסט שלך על ידי" - liked: "אהב את הפוסט שלך" - invited_to_topic: "הוזמנת לנושא חדש מ" - invitee_accepted: "הזמנה התקבלה על ידי" - moved_post: "הפוסט שלך הוזז על ידי" - linked: "קישור לפוסט שלך" - granted_badge: "עיטור הוענק" - group_message_summary: "הודעות בדואר-נכנס קבוצתי" popup: mentioned: '{{username}} הזכיר/ה אותך ב{{topic}}" - {{site_title}}"' group_mentioned: '{{username}} הזכיר/ה אתכם ב "{{topic}}" - {{site_title}}' diff --git a/config/locales/client.id.yml b/config/locales/client.id.yml index d2b34522be..b39ce0eca4 100644 --- a/config/locales/client.id.yml +++ b/config/locales/client.id.yml @@ -787,12 +787,6 @@ id: notifications: empty: "Tidak ada pemberitahuan." more: "lihat notifikasi sebelumnya" - alt: - mentioned: "Disebut oleh" - replied: "Telah dibalas" - posted: "Tulisan oleh" - linked: "Tautkan pada tulisan anda" - group_message_summary: "Pesan di kotak pesan grup" new_item: "baru" topics: bulk: diff --git a/config/locales/client.it.yml b/config/locales/client.it.yml index b2a73d4e88..bbd7059f6a 100644 --- a/config/locales/client.it.yml +++ b/config/locales/client.it.yml @@ -1179,20 +1179,6 @@ it: group_message_summary: one: "{{count}} messaggi in arrivo nella casella {{group_name}}" other: "{{count}} messaggi in arrivo nella casella {{group_name}}" - alt: - mentioned: "Menzionato da" - quoted: "Citato da" - replied: "Risposto" - posted: "Messaggio da" - edited: "Modifica il tuo messaggio da" - liked: "Ha assegnato un \"Mi piace\" al tuo messaggio" - invited_to_topic: "Invitato a un argomento da" - invitee_accepted: "Invito accettato da" - moved_post: "Il tuo messaggio è stato spostato da" - linked: "Collegamento al tuo messaggio" - granted_badge: "Distintivo assegnato" - group_message_summary: "Messaggi nel gruppo in arrivo" - topic_reminder: "Un promemoria" popup: mentioned: '{{username}} ti ha menzionato in "{{topic}}" - {{site_title}}' group_mentioned: '{{username}} ti ha menzionato in "{{topic}}" - {{site_title}}' diff --git a/config/locales/client.ja.yml b/config/locales/client.ja.yml index 8d4fbe9e65..edf3989be4 100644 --- a/config/locales/client.ja.yml +++ b/config/locales/client.ja.yml @@ -1114,19 +1114,6 @@ ja: watching_first_post: "新しいトピック {{description}}" group_message_summary: other: "{{group_name}} 宛のメッセージが {{count}} 通あります" - alt: - mentioned: "メンションされました: " - quoted: "引用されました: " - replied: "リプライ" - posted: "投稿者 " - edited: "投稿の編集者: " - liked: "あなたの投稿をいいねしました" - invited_to_topic: "トピックに招待されました: " - invitee_accepted: "招待が承認されました: " - moved_post: "投稿を移動しました: " - linked: "あなたの投稿にリンク" - granted_badge: "バッジを付与" - group_message_summary: "グループ宛のメッセージがあります" popup: mentioned: '"{{topic}}"で{{username}} にメンションされました - {{site_title}}' group_mentioned: '"{{topic}}"で{{username}} にメンションされました - {{site_title}}' diff --git a/config/locales/client.ko.yml b/config/locales/client.ko.yml index 1f58ba55c5..1edaad1830 100644 --- a/config/locales/client.ko.yml +++ b/config/locales/client.ko.yml @@ -1115,19 +1115,6 @@ ko: watching_first_post: "새 토픽 {{description}}" group_message_summary: other: " {{group_name}} 사서함에 {{count}} 개의 메시지가 있습니다" - alt: - mentioned: "멘션 by" - quoted: "인용 by" - replied: "답글을 전송했습니다." - posted: "포스트 by" - edited: "당신 글이 다음 이용자에 의해 수정" - liked: "당신의 글을 좋아했음." - invited_to_topic: "다음 사람으로부터 한 주제로 초대됨" - invitee_accepted: "다음 사람에 의해 초대가 수락됨." - moved_post: "다음 사람에 의해서 당신의 글이 이동됨" - linked: "당신 글로 링크하기" - granted_badge: "배지가 수여됨." - group_message_summary: "그룹 메시지함의 메시지" popup: mentioned: '"{{topic}}" - {{site_title}}에서 {{username}} 님이 나를 멘션했습니다' group_mentioned: '"{{topic}}" - {{site_title}}에서 {{username}} 님이 당신을 언급했습니다' diff --git a/config/locales/client.lv.yml b/config/locales/client.lv.yml index 4a6f0524b3..853672d34a 100644 --- a/config/locales/client.lv.yml +++ b/config/locales/client.lv.yml @@ -1136,19 +1136,6 @@ lv: empty: "Nav atrasti paziņojumi." more: "skatīt senākus paziņojumus" total_flagged: "pavisam ieraksti ar sūdzībam" - alt: - mentioned: "Pieminēja" - quoted: "Citēja" - replied: "Atbildēja" - posted: "Ieraksts" - edited: "Korektētēja" - liked: "Pauda atzinību jūsu ierakstam" - invited_to_topic: "Ielūgums uz tēmu no" - invitee_accepted: "Ielūgumu pieņēma " - moved_post: "Jūsu ierakstu pārvietoja" - linked: "Saite uz jūsu ierakstu" - granted_badge: "Žetonu piešķīra" - group_message_summary: "Ziņas grupas iesūtnē" popup: mentioned: '{{username}} pieminēja jūs "{{topic}}" - {{site_title}}' group_mentioned: '{{username}} pieminēja jūs "{{topic}}" - {{site_title}}' diff --git a/config/locales/client.nb_NO.yml b/config/locales/client.nb_NO.yml index ad3814ae96..0e0ac27988 100644 --- a/config/locales/client.nb_NO.yml +++ b/config/locales/client.nb_NO.yml @@ -1264,22 +1264,6 @@ nb_NO: group_message_summary: one: "{{count}} melding i din {{group_name}} innboks" other: "{{count}} meldinger i din innboks for {{group_name}}" - alt: - mentioned: "Nevnt av" - quoted: "Sitert av" - replied: "Svart" - posted: "Innlegg av" - edited: "Rediger ditt innlegg innen" - liked: "Likte innlegget ditt" - private_message: "Personlig melding fra" - invited_to_private_message: "Invitert til en personlig melding fra" - invited_to_topic: "Invitert til en tråd fra" - invitee_accepted: "Invitiasjon akseptert av" - moved_post: "Ditt innlegg ble flyttet av" - linked: "Link til innlegget ditt" - granted_badge: "Merke innvilget" - group_message_summary: "Meldinger i gruppeinnboks" - topic_reminder: "En påminnelse" popup: mentioned: '{{username}} nevnte deg i "{{topic}}" - {{site_title}}' group_mentioned: '{{username}} nevnte deg i "{{topic}}" - {{site_title}}' diff --git a/config/locales/client.nl.yml b/config/locales/client.nl.yml index fc697583f9..afd93bae3a 100644 --- a/config/locales/client.nl.yml +++ b/config/locales/client.nl.yml @@ -1142,19 +1142,6 @@ nl: group_message_summary: one: "{{count}} bericht in je Postvak IN voor {{group_name}}" other: "{{count}} berichten in je Postvak IN voor {{group_name}}" - alt: - mentioned: "Genoemd door" - quoted: "Geciteerd door" - replied: "Beantwoord" - posted: "Geplaatst door" - edited: "Uw bericht bewerkt door" - liked: "Heeft uw bericht geliket" - invited_to_topic: "Uitgenodigd voor een topic door" - invitee_accepted: "Uitnodiging geaccepteerd door" - moved_post: "Uw bericht is verplaatst door" - linked: "Koppeling naar uw bericht" - granted_badge: "Badge toegekend" - group_message_summary: "Berichten in groepspostvak" popup: mentioned: '{{username}} heeft u genoemd in ''{{topic}}'' - {{site_title}}' group_mentioned: '{{username}} heeft u genoemd in ''{{topic}}'' - {{site_title}}' diff --git a/config/locales/client.pl_PL.yml b/config/locales/client.pl_PL.yml index 8b3583c0d2..977eaba3ed 100644 --- a/config/locales/client.pl_PL.yml +++ b/config/locales/client.pl_PL.yml @@ -1250,20 +1250,6 @@ pl_PL: few: "masz {{count}} wiadomości w skrzynce odbiorczej {{group_name}}" many: "masz {{count}} wiadomości w skrzynce odbiorczej {{group_name}}" other: "masz {{count}} wiadomości w skrzynce odbiorczej {{group_name}}" - alt: - mentioned: "Wywołanie przez" - quoted: "Cytowanie przez" - replied: "Odpowiedź" - posted: "Autor wpisu" - edited: "Edycja twojego wpisu" - liked: "Lajk twojego wpisu" - invited_to_topic: "Zaproszenie do tematu od" - invitee_accepted: "Zaproszenie zaakceptowane przez" - moved_post: "Twój wpis został przeniesiony przez" - linked: "Linkownie do twojego wpisu" - granted_badge: "Przyznanie odznaki" - group_message_summary: "Wiadomości w grupowej skrzynce odbiorczej" - topic_reminder: "Przypomnienie" popup: mentioned: '{{username}} wspomina o tobie w "{{topic}}" - {{site_title}}' group_mentioned: '{{username}} wspomniał o Tobie w "{{topic}}" - {{site_title}}' diff --git a/config/locales/client.pt.yml b/config/locales/client.pt.yml index 0941d8f296..aa01eb03c4 100644 --- a/config/locales/client.pt.yml +++ b/config/locales/client.pt.yml @@ -1175,22 +1175,6 @@ pt: linked: "{{username}} {{description}}" topic_reminder: "{{username}} {{description}}" watching_first_post: "Novo Tópico {{description}}" - alt: - mentioned: "Mencionado por" - quoted: "Citado por" - replied: "Respondido" - posted: "Publicado por" - edited: "Edição da sua publicação por" - liked: "Gostou da sua publicação" - private_message: "Mensagem privada de" - invited_to_private_message: "Convidado para uma mensagem privada de" - invited_to_topic: "Convidado para um tópico de" - invitee_accepted: "Convite aceite por" - moved_post: "A sua publicação foi movida por" - linked: "Hiperligação para a sua publicação" - granted_badge: "Distintivo concedido" - group_message_summary: "Mensagens na caixa de entrada do seu grupo" - topic_reminder: "Um lembrete" popup: mentioned: '{{username}} mencionou-o em "{{topic}}" - {{site_title}}' group_mentioned: '{{username}} mencionou-o em "{{topic}}" - {{site_title}}' diff --git a/config/locales/client.pt_BR.yml b/config/locales/client.pt_BR.yml index 263812d7bf..754b85a8b9 100644 --- a/config/locales/client.pt_BR.yml +++ b/config/locales/client.pt_BR.yml @@ -1116,20 +1116,6 @@ pt_BR: granted_badge: "Adquiriu '{{description}}'" topic_reminder: "{{username}} {{description}}" watching_first_post: "Novo Tópico {{description}}" - alt: - mentioned: "Mencionado por" - quoted: "Citado por" - replied: "Respondido" - posted: "Mensagem por" - edited: "Edição na sua mensagem por" - liked: "Curtiu sua mensagem" - invited_to_topic: "Convite para um tópico de" - invitee_accepted: "Convite aceito por" - moved_post: "Seu tópico foi movido por" - linked: "Link para sua mensagem" - granted_badge: "Emblema concedido" - group_message_summary: "Mensagens na caixa de entrada do grupo" - topic_reminder: "Um lembrete" popup: mentioned: '{{username}} mencionou você em "{{topic}}" - {{site_title}}' group_mentioned: '{{username}} mencionou você em "{{topic}}" - {{site_title}}' diff --git a/config/locales/client.ro.yml b/config/locales/client.ro.yml index 8f47aed309..bda5ac6c79 100644 --- a/config/locales/client.ro.yml +++ b/config/locales/client.ro.yml @@ -1218,21 +1218,6 @@ ro: granted_badge: "Ai câștigat '{{description}}'" topic_reminder: "{{username}} {{description}}" watching_first_post: "Subiect nou {{description}}" - alt: - mentioned: "Menționat de" - quoted: "Citat de" - replied: "Răspuns" - posted: "Postat de" - edited: "Editează postarea prin" - liked: "V-a apreciat postarea" - private_message: "Mesaj personal de la" - invited_to_private_message: "Invitat la un mesaj privat de către" - invited_to_topic: "Invitat la un subiect de către" - invitee_accepted: "Invitație acceptată de către" - moved_post: "Postarea ta a fost mutată de către" - linked: "Link spre postarea ta" - granted_badge: "Ecuson acordat" - group_message_summary: "Mesaje în căsuța poștală a grupului" popup: mentioned: '{{username}} te-a menţionat în "{{topic}}" - {{site_title}}' group_mentioned: '{{username}} te-a menţionat în "{{topic}}" - {{site_title}}' diff --git a/config/locales/client.ru.yml b/config/locales/client.ru.yml index 2a65929fb8..ccc0f75159 100644 --- a/config/locales/client.ru.yml +++ b/config/locales/client.ru.yml @@ -1221,20 +1221,6 @@ ru: few: "{{count}} сообщений в вашей группе: {{group_name}} " many: "{{count}} сообщений в вашей группе: {{group_name}} " other: "{{count}} сообщений в вашей группе: {{group_name}} " - alt: - mentioned: "Упомянуто" - quoted: "Процитировано пользователем" - replied: "Ответил" - posted: "Опубликовано" - edited: "Изменил ваше сообщение" - liked: "Понравилось ваше сообщение" - private_message: "Личное сообщение от" - invited_to_topic: "Приглашение в тему от" - invitee_accepted: "Приглашение принято" - moved_post: "Ваша тема перенесена участником " - linked: "Ссылка на ваше сообщение" - granted_badge: "Награда получена от" - group_message_summary: "Входящие сообщения группы" popup: mentioned: '{{username}} упомянул вас в "{{topic}}" - {{site_title}}' group_mentioned: '{{username}} упомянул вас в "{{topic}}" - {{site_title}}' diff --git a/config/locales/client.sk.yml b/config/locales/client.sk.yml index ca8b1e3fe9..16d6440b1c 100644 --- a/config/locales/client.sk.yml +++ b/config/locales/client.sk.yml @@ -1098,20 +1098,6 @@ sk: empty: "Žiadne upozornenia sa nenašli." more: "zobraziť staršie upozornenia" total_flagged: "označených príspevkov celkom" - alt: - mentioned: "Zmienený od" - quoted: "Citovaný používateľom" - replied: "Odpovedané" - posted: "Príspevok od" - edited: "Upravte Váš príspevok do" - liked: "Váš príspevok sa páčil" - private_message: "Privátna správa od" - invited_to_topic: "Pozvaný k téme od" - invitee_accepted: "Pozvánka akceptovaná " - moved_post: "Váš príspevok bol presunutý " - linked: "Odkaz na váš príspevok" - granted_badge: "Udelený odznak" - group_message_summary: "Správy v skupinovej schránke" popup: mentioned: '{{username}} Vás zmienil v "{{topic}}" - {{site_title}}' group_mentioned: '{{username}} Vás zmienil v "{{topic}}" - {{site_title}}' diff --git a/config/locales/client.sl.yml b/config/locales/client.sl.yml index bee45566f5..1b858c420d 100644 --- a/config/locales/client.sl.yml +++ b/config/locales/client.sl.yml @@ -898,12 +898,6 @@ sl: invitee_accepted: "{{username}} je sprejel tvoje vabilo" linked: "{{username}} {{description}}" topic_reminder: "{{username}} {{description}}" - alt: - posted: "Objavil" - edited: "Urejeno s strani " - liked: "Všečkal tvojo objavo" - moved_post: "Tvojo objavo je premaknil " - linked: "Povezava do tvoje objave" search: sort_by: "Uredi po" relevance: "Relevanci" diff --git a/config/locales/client.sq.yml b/config/locales/client.sq.yml index fd24e12e3f..4889eab58a 100644 --- a/config/locales/client.sq.yml +++ b/config/locales/client.sq.yml @@ -1024,19 +1024,6 @@ sq: empty: "Nuk u gjet asnjë njoftim. " more: "shiko njoftimet e kaluara" total_flagged: "totali i postimeve të sinjalizuar" - alt: - mentioned: "Përmendur nga" - quoted: "Cituar nga" - replied: "Përgjigjur" - posted: "Postim nga" - edited: "Redakto postimin tuaj" - liked: "Pëlqeu postimin tuaj" - invited_to_topic: "I/e ftuar në temë nga" - invitee_accepted: "Ftesa u pranua nga" - moved_post: "Postimi juaj u transferua nga" - linked: "Lidhja drejt postimit tuaj" - granted_badge: "Stema u atribua" - group_message_summary: "Mesazhet në inboxin e grupit" popup: mentioned: '{{username}} ju përmendi në "{{topic}}" - {{site_title}}' group_mentioned: '{{username}} ju përmendi në "{{topic}}" - {{site_title}}' diff --git a/config/locales/client.sr.yml b/config/locales/client.sr.yml index 742fd3eb4f..a6d334d89a 100644 --- a/config/locales/client.sr.yml +++ b/config/locales/client.sr.yml @@ -855,8 +855,6 @@ sr: empty: "Nisu pronađene notifikacije" more: "pogledaj starije notifikacije" total_flagged: "sve ukupno označenih poruka" - alt: - replied: "Odgovoreno" upload_selector: title: "Dodaj sliku" title_with_attachments: "Dodaj sliku ili datoteku" diff --git a/config/locales/client.sv.yml b/config/locales/client.sv.yml index 197ded6863..95793d303f 100644 --- a/config/locales/client.sv.yml +++ b/config/locales/client.sv.yml @@ -1044,19 +1044,6 @@ sv: empty: "Inga notifieringar hittades." more: "visa äldre notifikationer" total_flagged: "totalt antal flaggade inlägg" - alt: - mentioned: "Omnämnd av" - quoted: "Citerad av" - replied: "Svarade" - posted: "Skrivet av" - edited: "Redigera ditt inlägg genom" - liked: "Gillade ditt inlägg" - invited_to_topic: "Inbjudan till ett ämne från" - invitee_accepted: "Inbjudan accepterades av" - moved_post: "Ditt inlägg blev flyttad av" - linked: "Länk till ditt inlägg" - granted_badge: "Utmärkelse beviljad" - group_message_summary: "Meddelanden i din grupps inkorg" popup: mentioned: '{{username}} nämnde dig i "{{topic}}" - {{site_title}}' group_mentioned: '{{username}} nämnde dig i "{{topic}}" - {{site_title}}' diff --git a/config/locales/client.th.yml b/config/locales/client.th.yml index c1d8722fa8..9dfedfb281 100644 --- a/config/locales/client.th.yml +++ b/config/locales/client.th.yml @@ -917,19 +917,6 @@ th: none: "ไม่สามารถโหลดการแจ้งเตือนในขณะนี้" more: "ดูการแจ้งเตือนก่อนหน้านี้" total_flagged: "โพสที่ปักธงทั้งหมด" - alt: - mentioned: "ถูกพูดถึงโดย" - quoted: "ถูกอ้างอิงโดย" - replied: "ตอบแล้ว" - posted: "โพสโดย" - edited: "แก้ไขโพสของคุณโดย" - liked: "ได้ถูกใจโพสของคุณ" - invited_to_topic: "เชิญไปยังกระทู้จาก" - invitee_accepted: "การเชิญได้ตอบรับโดย" - moved_post: "โพสของคุณได้ถูกย้ายโดย" - linked: "ลิงค์ไปยังโพสของคุณ" - granted_badge: "ได้รับป้าย" - group_message_summary: "ข้อความในกล่องข้อความกลุ่ม" popup: mentioned: '{{username}} พูดถึงคุณใน "{{topic}}" - {{site_title}}' group_mentioned: '{{username}} พูดถึงคุณใน "{{topic}}" - {{site_title}}' diff --git a/config/locales/client.tr_TR.yml b/config/locales/client.tr_TR.yml index 9e42409f93..cc150a4b84 100644 --- a/config/locales/client.tr_TR.yml +++ b/config/locales/client.tr_TR.yml @@ -1177,20 +1177,6 @@ tr_TR: granted_badge: "'{{description}}' kazandı" topic_reminder: "{{username}} {{description}}" watching_first_post: "Yeni Konu {{description}}" - alt: - mentioned: "Bahsedildi, şu kişi tarafından" - quoted: "Alıntılandı, şu kişi tarafından" - replied: "Cevaplandı" - posted: "Gönderildi, şu kişi tarafından" - edited: "Gönderiniz düzenlendi, şu kişi tarafından" - liked: "Gönderiniz beğenildi" - invited_to_topic: "Bir konuya davet edildiniz, şu kişi tarafından" - invitee_accepted: "Davet kabul edildi, şu kişi tarafından" - moved_post: "Gönderiniz taşındı, şu kişi tarafından" - linked: "Gönderinize bağlantı" - granted_badge: "Rozet alındı" - group_message_summary: "Grup gelen kutusundaki iletiler" - topic_reminder: "Bir hatırlatıcı" popup: mentioned: '{{username}}, "{{topic}}" başlıklı konuda sizden bahsetti - {{site_title}}' group_mentioned: '{{username}} sizden bahsetti "{{topic}}" - {{site_title}}' diff --git a/config/locales/client.uk.yml b/config/locales/client.uk.yml index 53f0cf5f3a..06499af225 100644 --- a/config/locales/client.uk.yml +++ b/config/locales/client.uk.yml @@ -658,8 +658,6 @@ uk: notifications: more: "переглянути старіші сповіщення" total_flagged: "всього дописів, на які поскаржилися" - alt: - posted: "Опубліковано" upload_selector: title: "Додати зображення" title_with_attachments: "Додати зображення або файл" diff --git a/config/locales/client.ur.yml b/config/locales/client.ur.yml index 6f089f082e..aa006b85f2 100644 --- a/config/locales/client.ur.yml +++ b/config/locales/client.ur.yml @@ -1273,22 +1273,6 @@ ur: group_message_summary: one: "آپ کے {{group_name}} اِن باکس میں {{count}} پیغام" other: "آپ کے {{group_name}} اِن باکس میں {{count}} پیغامات" - alt: - mentioned: "کی طرف سے ذکر کیا گیا" - quoted: "کی طرف سے اقتباس کیا گیا" - replied: "جواب دیا" - posted: "کی طرف سے پوسٹ" - edited: "اِس وقت تک اپنی پوسٹ میں ترمیم کر لیں" - liked: "آپ کی پوسٹ کو لائیک کیا" - private_message: "کی طرف سے ذاتی پیغام" - invited_to_private_message: "کی طرف سے ذاتی پیغام کیلئے دعوت دی گئی" - invited_to_topic: "کی طرف سے ٹاپک کیلئے دعوت دی گئی" - invitee_accepted: "کی طرف سے دعوت قبول کر لی گئی" - moved_post: "کی طرف سے آپ کی پوسٹ منتقل کر دی گئی تھی" - linked: "آپ کی پوسٹ سے لنک کیا" - granted_badge: "بَیج عطا کیا" - group_message_summary: "گروپ اِن باکس میں پیغامات" - topic_reminder: "ایک یاد دہانی" popup: mentioned: '{{username}} نے آپ کا تذکرہ "{{topic}}" میں کیا - {{site_title}}' group_mentioned: '{{username}} نے آپ کا تذکرہ "{{topic}}" میں کیا - {{site_title}}' diff --git a/config/locales/client.vi.yml b/config/locales/client.vi.yml index 7e9f1df5ed..d496bfcbc9 100644 --- a/config/locales/client.vi.yml +++ b/config/locales/client.vi.yml @@ -1040,19 +1040,6 @@ vi: empty: "Không có thông báo" more: "xem thông báo cũ hơn" total_flagged: "tổng số bài viết gắn cờ" - alt: - mentioned: "Được nhắc đến bởi" - quoted: "Trích dẫn bởi" - replied: "Đã trả lời" - posted: "Đăng bởi" - edited: "Bài viết của bạn được sửa bởi" - liked: "Bạn đã like bài viết" - invited_to_topic: "Lời mời tham gia chủ đề từ" - invitee_accepted: "Lời mời được chấp nhận bởi" - moved_post: "Bài viết của bạn đã được di chuyển bởi" - linked: "Liên kết đến bài viết của bạn" - granted_badge: "Cấp huy hiệu" - group_message_summary: "Tin nhắn trong hộp thư đến" popup: mentioned: '{{username}} nhắc đến bạn trong "{{topic}}" - {{site_title}}' group_mentioned: '{{username}} nhắc đến bạn trong "{{topic}}" - {{site_title}}' diff --git a/config/locales/client.zh_CN.yml b/config/locales/client.zh_CN.yml index 0244c7a345..2322a764f7 100644 --- a/config/locales/client.zh_CN.yml +++ b/config/locales/client.zh_CN.yml @@ -1138,20 +1138,6 @@ zh_CN: watching_first_post: "新主题 {{description}}" group_message_summary: other: "{{count}} 条私信在{{group_name}}组的收件箱中" - alt: - mentioned: "被提及" - quoted: "被引用" - replied: "回复" - posted: "发自" - edited: "编辑你的帖子" - liked: "赞了你的帖子" - invited_to_topic: "主题邀请自" - invitee_accepted: "介绍邀请自" - moved_post: "你的帖子被移动自" - linked: "链接至你的帖子" - granted_badge: "勋章授予" - group_message_summary: "在群组收件箱中的私信" - topic_reminder: "一个提醒" popup: mentioned: '{{username}}在“{{topic}}”提到了你 - {{site_title}}' group_mentioned: '{{username}}在“{{topic}}”提到了你 - {{site_title}}' diff --git a/config/locales/client.zh_TW.yml b/config/locales/client.zh_TW.yml index 406e984a4f..9797c027e4 100644 --- a/config/locales/client.zh_TW.yml +++ b/config/locales/client.zh_TW.yml @@ -1005,19 +1005,6 @@ zh_TW: empty: "未找到任何通知。" more: "檢視較舊的通知" total_flagged: "所有被投訴的文章" - alt: - mentioned: "被提及" - quoted: "引用者" - replied: "回覆" - posted: "發自" - edited: "編輯你的帖子" - liked: "讚了你的帖子" - invited_to_topic: "主題邀請自" - invitee_accepted: "介紹邀請自" - moved_post: "你的帖子被移動自" - linked: "連結到你的討論" - granted_badge: "勛章授予" - group_message_summary: "在群組收件箱中的消息" popup: mentioned: '{{username}}在“{{topic}}”提到了你 - {{site_title}}' group_mentioned: '{{username}}在“{{topic}}”提到了你 - {{site_title}}' From 31d0998506aab82432e6df58e094af9a3450b3b2 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 28 Mar 2018 12:32:16 -0400 Subject: [PATCH 063/287] FIX: Don't allow links with no href --- app/models/post_analyzer.rb | 4 ++-- spec/models/post_analyzer_spec.rb | 5 +++++ spec/models/post_spec.rb | 1 - 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/models/post_analyzer.rb b/app/models/post_analyzer.rb index ab8ec1903d..5d069c72eb 100644 --- a/app/models/post_analyzer.rb +++ b/app/models/post_analyzer.rb @@ -111,9 +111,9 @@ class PostAnalyzer return @raw_links if @raw_links.present? @raw_links = [] - cooked_stripped.css("a[href]").each do |l| + cooked_stripped.css("a").each do |l| # Don't include @mentions in the link count - next if l['href'].blank? || link_is_a_mention?(l) + next if link_is_a_mention?(l) @raw_links << l['href'].to_s end diff --git a/spec/models/post_analyzer_spec.rb b/spec/models/post_analyzer_spec.rb index 937b47687c..f7e20efc1e 100644 --- a/spec/models/post_analyzer_spec.rb +++ b/spec/models/post_analyzer_spec.rb @@ -176,6 +176,11 @@ describe PostAnalyzer do expect(post_analyzer.link_count).to eq(0) end + it "returns links with href=''" do + post_analyzer = PostAnalyzer.new('Hello world', nil) + expect(post_analyzer.link_count).to eq(1) + end + it "finds links from markdown" do Oneboxer.stubs :onebox post_analyzer = PostAnalyzer.new(raw_post_one_link_md, default_topic_id) diff --git a/spec/models/post_spec.rb b/spec/models/post_spec.rb index 9336d2ad1b..2c1d237b8a 100644 --- a/spec/models/post_spec.rb +++ b/spec/models/post_spec.rb @@ -410,7 +410,6 @@ describe Post do end it "finds links from HTML" do - expect(post_two_links.link_count).to eq(2) end From fa608f2bb4e25d265f9624e5150550d7dae9089d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Wed, 28 Mar 2018 18:57:11 +0200 Subject: [PATCH 064/287] FIX: ensure theme variables are unique when adding an upload --- .../admin/controllers/modals/admin-add-upload.js.es6 | 10 +++++++--- config/locales/client.en.yml | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/admin/controllers/modals/admin-add-upload.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-add-upload.js.es6 index 7bbdaa3de5..2eb6993b2f 100644 --- a/app/assets/javascripts/admin/controllers/modals/admin-add-upload.js.es6 +++ b/app/assets/javascripts/admin/controllers/modals/admin-add-upload.js.es6 @@ -3,6 +3,8 @@ import { ajax } from 'discourse/lib/ajax'; import { default as computed, observes } from 'ember-addons/ember-computed-decorators'; import { popupAjaxError } from 'discourse/lib/ajax-error'; +const THEME_FIELD_VARIABLE_TYPE_IDS = [2, 3, 4]; + export default Ember.Controller.extend(ModalFunctionality, { adminCustomizeThemesShow: Ember.inject.controller(), @@ -14,9 +16,11 @@ export default Ember.Controller.extend(ModalFunctionality, { enabled: Em.computed.and('nameValid', 'fileSelected'), disabled: Em.computed.not('enabled'), - @computed('name') - nameValid(name) { - return name && name.match(/^[a-z_][a-z0-9_-]*$/i); + @computed('name', 'adminCustomizeThemesShow.model.theme_fields') + nameValid(name, themeFields) { + return name && + name.match(/^[a-z_][a-z0-9_-]*$/i) && + !themeFields.some(tf => THEME_FIELD_VARIABLE_TYPE_IDS.includes(tf.type_id) && name === tf.name); }, @observes('name') diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index b5d2236856..52bf9d2469 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -3099,7 +3099,7 @@ en: add_upload: "Add Upload" upload_file_tip: "Choose an asset to upload (png, woff2, etc...)" variable_name: "SCSS var name:" - variable_name_invalid: "Invalid variable name. Only alphanumeric allowed. Must start with a letter." + variable_name_invalid: "Invalid variable name. Only alphanumeric allowed. Must start with a letter. Must be unique." upload: "Upload" child_themes_check: "Theme includes other child themes" css_html: "Custom CSS/HTML" From efedd9745fd9c34ac982941cca79f8ab8e517328 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 28 Mar 2018 13:57:20 -0400 Subject: [PATCH 065/287] PERF: Don't join on shared drafts unless you have to --- app/controllers/list_controller.rb | 5 +++- app/models/topic_list.rb | 27 ++++++++++--------- app/serializers/topic_list_item_serializer.rb | 5 +++- app/serializers/topic_list_serializer.rb | 18 +++++++++++-- lib/topic_query.rb | 14 +++++----- 5 files changed, 47 insertions(+), 22 deletions(-) diff --git a/app/controllers/list_controller.rb b/app/controllers/list_controller.rb index 659e66f008..4971861a65 100644 --- a/app/controllers/list_controller.rb +++ b/app/controllers/list_controller.rb @@ -71,7 +71,10 @@ class ListController < ApplicationController list = TopicQuery.new(user, list_opts).public_send("list_#{filter}") - if @category.present? && guardian.can_create_shared_draft? + if @category.present? && + guardian.can_create_shared_draft? && + @category.id != SiteSetting.shared_drafts_category.to_i + shared_drafts = TopicQuery.new( user, category: SiteSetting.shared_drafts_category, diff --git a/app/models/topic_list.rb b/app/models/topic_list.rb index 6de3eafcbe..5aae90f778 100644 --- a/app/models/topic_list.rb +++ b/app/models/topic_list.rb @@ -26,18 +26,21 @@ class TopicList end end - attr_accessor :more_topics_url, - :prev_topics_url, - :draft, - :draft_key, - :draft_sequence, - :filter, - :for_period, - :per_page, - :top_tags, - :current_user, - :tags, - :shared_drafts + attr_accessor( + :more_topics_url, + :prev_topics_url, + :draft, + :draft_key, + :draft_sequence, + :filter, + :for_period, + :per_page, + :top_tags, + :current_user, + :tags, + :shared_drafts, + :category + ) def initialize(filter, current_user, topics, opts = nil) @filter = filter diff --git a/app/serializers/topic_list_item_serializer.rb b/app/serializers/topic_list_item_serializer.rb index 663cc2c9a2..a454437434 100644 --- a/app/serializers/topic_list_item_serializer.rb +++ b/app/serializers/topic_list_item_serializer.rb @@ -1,6 +1,8 @@ class TopicListItemSerializer < ListableTopicSerializer include TopicTagsMixin + attr_accessor :include_destination_category + attributes :views, :like_count, :has_summary, @@ -30,8 +32,9 @@ class TopicListItemSerializer < ListableTopicSerializer end def category_id + # If it's a shared draft, show the destination topic instead - if object.category_id == SiteSetting.shared_drafts_category.to_i && object.shared_draft + if include_destination_category && object.shared_draft return object.shared_draft.category_id end diff --git a/app/serializers/topic_list_serializer.rb b/app/serializers/topic_list_serializer.rb index 6ee27854ca..f6dd513bf3 100644 --- a/app/serializers/topic_list_serializer.rb +++ b/app/serializers/topic_list_serializer.rb @@ -9,12 +9,26 @@ class TopicListSerializer < ApplicationSerializer :per_page, :top_tags, :tags, - :shared_drafts + :shared_drafts, + :topics - has_many :topics, serializer: TopicListItemSerializer, embed: :objects has_many :shared_drafts, serializer: TopicListItemSerializer, embed: :objects has_many :tags, serializer: TagSerializer, embed: :objects + def topics + object.topics.map do |t| + serializer = TopicListItemSerializer.new(t, scope: scope, root: false) + + if scope.can_create_shared_draft? && + object.category&.id == SiteSetting.shared_drafts_category.to_i + + serializer.include_destination_category = true + end + + serializer + end + end + def can_create_topic scope.can_create?(Topic) end diff --git a/lib/topic_query.rb b/lib/topic_query.rb index a6cbacf3f4..563eedf67a 100644 --- a/lib/topic_query.rb +++ b/lib/topic_query.rb @@ -482,17 +482,19 @@ class TopicQuery end def apply_shared_drafts(result, category_id, options) - viewing_shared = category_id && category_id == SiteSetting.shared_drafts_category.to_i + drafts_category_id = SiteSetting.shared_drafts_category.to_i + viewing_shared = category_id && category_id == drafts_category_id if guardian.can_create_shared_draft? - result = result.includes(:shared_draft).references(:shared_draft) - if options[:destination_category_id] destination_category_id = get_category_id(options[:destination_category_id]) - return result.where("shared_drafts.category_id" => destination_category_id) + topic_ids = SharedDraft.where(category_id: destination_category_id).pluck(:topic_id) + return result.where(id: topic_ids) + elsif viewing_shared + result = result.includes(:shared_draft).references(:shared_draft) + else + return result.where('topics.category_id != ?', drafts_category_id) end - - return result.where("shared_drafts.id IS NULL") unless viewing_shared end result From 6aef8f9cd9ece04db1170f0d13e71f0d5150db7f Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 28 Mar 2018 14:05:09 -0400 Subject: [PATCH 066/287] Add an index on category_id to shared drafts --- .../20180328180317_add_category_index_to_shared_drafts.rb | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 db/migrate/20180328180317_add_category_index_to_shared_drafts.rb diff --git a/db/migrate/20180328180317_add_category_index_to_shared_drafts.rb b/db/migrate/20180328180317_add_category_index_to_shared_drafts.rb new file mode 100644 index 0000000000..cd1be7b23d --- /dev/null +++ b/db/migrate/20180328180317_add_category_index_to_shared_drafts.rb @@ -0,0 +1,5 @@ +class AddCategoryIndexToSharedDrafts < ActiveRecord::Migration[5.1] + def change + add_index :shared_drafts, :category_id + end +end From a8f211bd4120913d7019f692674edd52128c4eff Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 28 Mar 2018 14:48:14 -0400 Subject: [PATCH 067/287] Extensibility for custom staff check --- lib/staff_constraint.rb | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/staff_constraint.rb b/lib/staff_constraint.rb index ba4dc0a36c..7572ada16c 100644 --- a/lib/staff_constraint.rb +++ b/lib/staff_constraint.rb @@ -4,9 +4,17 @@ class StaffConstraint def matches?(request) provider = Discourse.current_user_provider.new(request.env) - provider.current_user && provider.current_user.staff? + provider.current_user && + provider.current_user.staff? && + custom_staff_check(request) rescue Discourse::InvalidAccess false end + # Extensibility point: plugins can overwrite this to add additional checks + # if they require. + def custom_staff_check(request) + true + end + end From 4b5977aa6a3b0ccd67efd92c8d319ba99dca30fa Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 28 Mar 2018 15:35:13 -0400 Subject: [PATCH 068/287] Revert "PERF: Don't join on shared drafts unless you have to" This reverts commit efedd9745fd9c34ac982941cca79f8ab8e517328. --- app/controllers/list_controller.rb | 5 +--- app/models/topic_list.rb | 27 +++++++++---------- app/serializers/topic_list_item_serializer.rb | 5 +--- app/serializers/topic_list_serializer.rb | 18 ++----------- lib/topic_query.rb | 14 +++++----- 5 files changed, 22 insertions(+), 47 deletions(-) diff --git a/app/controllers/list_controller.rb b/app/controllers/list_controller.rb index 4971861a65..659e66f008 100644 --- a/app/controllers/list_controller.rb +++ b/app/controllers/list_controller.rb @@ -71,10 +71,7 @@ class ListController < ApplicationController list = TopicQuery.new(user, list_opts).public_send("list_#{filter}") - if @category.present? && - guardian.can_create_shared_draft? && - @category.id != SiteSetting.shared_drafts_category.to_i - + if @category.present? && guardian.can_create_shared_draft? shared_drafts = TopicQuery.new( user, category: SiteSetting.shared_drafts_category, diff --git a/app/models/topic_list.rb b/app/models/topic_list.rb index 5aae90f778..6de3eafcbe 100644 --- a/app/models/topic_list.rb +++ b/app/models/topic_list.rb @@ -26,21 +26,18 @@ class TopicList end end - attr_accessor( - :more_topics_url, - :prev_topics_url, - :draft, - :draft_key, - :draft_sequence, - :filter, - :for_period, - :per_page, - :top_tags, - :current_user, - :tags, - :shared_drafts, - :category - ) + attr_accessor :more_topics_url, + :prev_topics_url, + :draft, + :draft_key, + :draft_sequence, + :filter, + :for_period, + :per_page, + :top_tags, + :current_user, + :tags, + :shared_drafts def initialize(filter, current_user, topics, opts = nil) @filter = filter diff --git a/app/serializers/topic_list_item_serializer.rb b/app/serializers/topic_list_item_serializer.rb index a454437434..663cc2c9a2 100644 --- a/app/serializers/topic_list_item_serializer.rb +++ b/app/serializers/topic_list_item_serializer.rb @@ -1,8 +1,6 @@ class TopicListItemSerializer < ListableTopicSerializer include TopicTagsMixin - attr_accessor :include_destination_category - attributes :views, :like_count, :has_summary, @@ -32,9 +30,8 @@ class TopicListItemSerializer < ListableTopicSerializer end def category_id - # If it's a shared draft, show the destination topic instead - if include_destination_category && object.shared_draft + if object.category_id == SiteSetting.shared_drafts_category.to_i && object.shared_draft return object.shared_draft.category_id end diff --git a/app/serializers/topic_list_serializer.rb b/app/serializers/topic_list_serializer.rb index f6dd513bf3..6ee27854ca 100644 --- a/app/serializers/topic_list_serializer.rb +++ b/app/serializers/topic_list_serializer.rb @@ -9,26 +9,12 @@ class TopicListSerializer < ApplicationSerializer :per_page, :top_tags, :tags, - :shared_drafts, - :topics + :shared_drafts + has_many :topics, serializer: TopicListItemSerializer, embed: :objects has_many :shared_drafts, serializer: TopicListItemSerializer, embed: :objects has_many :tags, serializer: TagSerializer, embed: :objects - def topics - object.topics.map do |t| - serializer = TopicListItemSerializer.new(t, scope: scope, root: false) - - if scope.can_create_shared_draft? && - object.category&.id == SiteSetting.shared_drafts_category.to_i - - serializer.include_destination_category = true - end - - serializer - end - end - def can_create_topic scope.can_create?(Topic) end diff --git a/lib/topic_query.rb b/lib/topic_query.rb index 563eedf67a..a6cbacf3f4 100644 --- a/lib/topic_query.rb +++ b/lib/topic_query.rb @@ -482,19 +482,17 @@ class TopicQuery end def apply_shared_drafts(result, category_id, options) - drafts_category_id = SiteSetting.shared_drafts_category.to_i - viewing_shared = category_id && category_id == drafts_category_id + viewing_shared = category_id && category_id == SiteSetting.shared_drafts_category.to_i if guardian.can_create_shared_draft? + result = result.includes(:shared_draft).references(:shared_draft) + if options[:destination_category_id] destination_category_id = get_category_id(options[:destination_category_id]) - topic_ids = SharedDraft.where(category_id: destination_category_id).pluck(:topic_id) - return result.where(id: topic_ids) - elsif viewing_shared - result = result.includes(:shared_draft).references(:shared_draft) - else - return result.where('topics.category_id != ?', drafts_category_id) + return result.where("shared_drafts.category_id" => destination_category_id) end + + return result.where("shared_drafts.id IS NULL") unless viewing_shared end result From eab64710ffd496b63be9584f07089887327090fd Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 28 Mar 2018 15:36:12 -0400 Subject: [PATCH 069/287] FIX: Shared draft performance fix + missing avatars --- app/controllers/list_controller.rb | 25 +++++++++++------ app/models/topic.rb | 2 +- app/models/topic_list.rb | 27 ++++++++++--------- app/serializers/topic_list_item_serializer.rb | 3 ++- lib/topic_query.rb | 14 +++++----- 5 files changed, 43 insertions(+), 28 deletions(-) diff --git a/app/controllers/list_controller.rb b/app/controllers/list_controller.rb index 659e66f008..bf07fdc028 100644 --- a/app/controllers/list_controller.rb +++ b/app/controllers/list_controller.rb @@ -71,15 +71,24 @@ class ListController < ApplicationController list = TopicQuery.new(user, list_opts).public_send("list_#{filter}") - if @category.present? && guardian.can_create_shared_draft? - shared_drafts = TopicQuery.new( - user, - category: SiteSetting.shared_drafts_category, - destination_category_id: list_opts[:category] - ).list_latest + if guardian.can_create_shared_draft? && @category.present? + if @category.id == SiteSetting.shared_drafts_category.to_i + # On shared drafts, show the destination category + list.topics.each do |t| + t.includes_destination_category = true + end + else + # When viewing a non-shared draft category, find topics whose + # destination are this category + shared_drafts = TopicQuery.new( + user, + category: SiteSetting.shared_drafts_category, + destination_category_id: list_opts[:category] + ).list_latest - if shared_drafts.present? && shared_drafts.topics.present? - list.shared_drafts = shared_drafts.topics + if shared_drafts.present? && shared_drafts.topics.present? + list.shared_drafts = shared_drafts.topics + end end end diff --git a/app/models/topic.rb b/app/models/topic.rb index 3ab042196a..cb6e1e2368 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -31,7 +31,7 @@ class Topic < ActiveRecord::Base def_delegator :notifier, :mute!, :notify_muted! def_delegator :notifier, :toggle_mute, :toggle_mute - attr_accessor :allowed_user_ids, :tags_changed + attr_accessor :allowed_user_ids, :tags_changed, :includes_destination_category DiscourseEvent.on(:site_setting_saved) do |site_setting| if site_setting.name.to_s == "slug_generation_method" && site_setting.saved_change_to_value? diff --git a/app/models/topic_list.rb b/app/models/topic_list.rb index 6de3eafcbe..5aae90f778 100644 --- a/app/models/topic_list.rb +++ b/app/models/topic_list.rb @@ -26,18 +26,21 @@ class TopicList end end - attr_accessor :more_topics_url, - :prev_topics_url, - :draft, - :draft_key, - :draft_sequence, - :filter, - :for_period, - :per_page, - :top_tags, - :current_user, - :tags, - :shared_drafts + attr_accessor( + :more_topics_url, + :prev_topics_url, + :draft, + :draft_key, + :draft_sequence, + :filter, + :for_period, + :per_page, + :top_tags, + :current_user, + :tags, + :shared_drafts, + :category + ) def initialize(filter, current_user, topics, opts = nil) @filter = filter diff --git a/app/serializers/topic_list_item_serializer.rb b/app/serializers/topic_list_item_serializer.rb index 663cc2c9a2..1f77b45d75 100644 --- a/app/serializers/topic_list_item_serializer.rb +++ b/app/serializers/topic_list_item_serializer.rb @@ -30,8 +30,9 @@ class TopicListItemSerializer < ListableTopicSerializer end def category_id + # If it's a shared draft, show the destination topic instead - if object.category_id == SiteSetting.shared_drafts_category.to_i && object.shared_draft + if object.includes_destination_category && object.shared_draft return object.shared_draft.category_id end diff --git a/lib/topic_query.rb b/lib/topic_query.rb index a6cbacf3f4..563eedf67a 100644 --- a/lib/topic_query.rb +++ b/lib/topic_query.rb @@ -482,17 +482,19 @@ class TopicQuery end def apply_shared_drafts(result, category_id, options) - viewing_shared = category_id && category_id == SiteSetting.shared_drafts_category.to_i + drafts_category_id = SiteSetting.shared_drafts_category.to_i + viewing_shared = category_id && category_id == drafts_category_id if guardian.can_create_shared_draft? - result = result.includes(:shared_draft).references(:shared_draft) - if options[:destination_category_id] destination_category_id = get_category_id(options[:destination_category_id]) - return result.where("shared_drafts.category_id" => destination_category_id) + topic_ids = SharedDraft.where(category_id: destination_category_id).pluck(:topic_id) + return result.where(id: topic_ids) + elsif viewing_shared + result = result.includes(:shared_draft).references(:shared_draft) + else + return result.where('topics.category_id != ?', drafts_category_id) end - - return result.where("shared_drafts.id IS NULL") unless viewing_shared end result From cf3c670cb70696e7bc3cc1819f11c24c2547241f Mon Sep 17 00:00:00 2001 From: Kris Date: Wed, 28 Mar 2018 18:49:19 -0400 Subject: [PATCH 070/287] Fixing table top-border alignment issue --- app/assets/stylesheets/common/base/_topic-list.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/common/base/_topic-list.scss b/app/assets/stylesheets/common/base/_topic-list.scss index a9033684d8..552e6f8e63 100644 --- a/app/assets/stylesheets/common/base/_topic-list.scss +++ b/app/assets/stylesheets/common/base/_topic-list.scss @@ -86,7 +86,7 @@ } } - > tbody > tr:first-of-type { + > tbody { border-top: 3px solid $primary-low; } From 98faf2878eae4454b0af4bc48fd6daee439b3ac8 Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 29 Mar 2018 11:12:09 +1100 Subject: [PATCH 071/287] FEATURE: bump rack-mini-profiler version This corrects a warning in chrome console and provides better jQuery 3 compatability --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 019b39ec03..0c2a0f404b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -257,7 +257,7 @@ GEM puma (3.9.1) r2 (0.2.6) rack (2.0.4) - rack-mini-profiler (0.10.7) + rack-mini-profiler (1.0.0) rack (>= 1.2.0) rack-openid (1.3.1) rack (>= 1.1.0) From 6e569e5bfd5fcbf90f2dd3aa336b4a982d954a08 Mon Sep 17 00:00:00 2001 From: Kris Date: Wed, 28 Mar 2018 20:47:04 -0400 Subject: [PATCH 072/287] X button for mobile edits in composer --- app/assets/javascripts/discourse/templates/composer.hbs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/templates/composer.hbs b/app/assets/javascripts/discourse/templates/composer.hbs index fdbad6c3e4..cdda936d57 100644 --- a/app/assets/javascripts/discourse/templates/composer.hbs +++ b/app/assets/javascripts/discourse/templates/composer.hbs @@ -104,11 +104,18 @@ label=model.saveLabel disableSubmit=disableSubmit}} {{#if site.mobileView}} - {{d-icon "trash-o"}} + + {{#if canEdit}} + {{d-icon "times"}} + {{else}} + {{d-icon "trash-o"}} + {{/if}} + {{else}} {{i18n 'cancel'}} {{/if}} + {{#if site.mobileView}} {{#if whisperOrUnlistTopic}} From 90f91bf01704162dd1e3e4e7f2eef807bd40a68f Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 28 Mar 2018 16:34:28 +0800 Subject: [PATCH 073/287] Fix regression due to https://github.com/discourse/discourse/commit/ee69d58a59c824a7a0b1146710930c5861d3ca26. --- app/jobs/regular/pull_hotlinked_images.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/jobs/regular/pull_hotlinked_images.rb b/app/jobs/regular/pull_hotlinked_images.rb index 0bbef3a470..4d00204e59 100644 --- a/app/jobs/regular/pull_hotlinked_images.rb +++ b/app/jobs/regular/pull_hotlinked_images.rb @@ -78,11 +78,11 @@ module Jobs log(:info, "Failed to pull hotlinked image for post: #{post_id}: #{src} - #{upload.errors.full_messages.join("\n")}") end else - large_images << remove_scheme(src) + large_images << remove_scheme(original_src) has_new_large_image = true end else - broken_images << remove_scheme(src) + broken_images << remove_scheme(original_src) has_new_broken_image = true end end From 3b3e6ed23f0d5316f33ba252d8e5538fbcd26318 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 29 Mar 2018 10:00:41 +0800 Subject: [PATCH 074/287] Fix deprecation warnings in Ember. See https://emberjs.com/deprecations/v2.x/#toc_ember-router-router-renamed-to-ember-router-_routermicrolib --- app/assets/javascripts/discourse/routes/discourse.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/routes/discourse.js.es6 b/app/assets/javascripts/discourse/routes/discourse.js.es6 index 169d50cd94..42595a0956 100644 --- a/app/assets/javascripts/discourse/routes/discourse.js.es6 +++ b/app/assets/javascripts/discourse/routes/discourse.js.es6 @@ -17,7 +17,7 @@ const DiscourseRoute = Ember.Route.extend({ refresh() { if (!this.refreshQueryWithoutTransition) { return this._super(); } - if (!this.router.router.activeTransition) { + if (!this.router._routerMicrolib.activeTransition) { const controller = this.controller, model = controller.get('model'), params = this.controller.getProperties(Object.keys(this.queryParams)); From 9260969101f7a48ff73307e9af8fa3e6be72f36a Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Thu, 29 Mar 2018 10:33:44 +0200 Subject: [PATCH 075/287] FIX: correctly shows education text for categories --- .../discourse/controllers/discovery/topics.js.es6 | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/discourse/controllers/discovery/topics.js.es6 b/app/assets/javascripts/discourse/controllers/discovery/topics.js.es6 index 8d3f4b4b86..344b79baff 100644 --- a/app/assets/javascripts/discourse/controllers/discovery/topics.js.es6 +++ b/app/assets/javascripts/discourse/controllers/discovery/topics.js.es6 @@ -137,11 +137,12 @@ const controllerOpts = { footerEducation: function() { if (!this.get('allLoaded') || this.get('model.topics.length') > 0 || !this.currentUser) { return; } - const split = (this.get('model.filter') || '').split('/'); + const segments = (this.get('model.filter') || '').split('/'); - if (split[0] !== 'new' && split[0] !== 'unread') { return; } + const tab = segments[segments.length - 1]; + if (tab !== 'new' && tab !== 'unread') { return; } - return I18n.t("topics.none.educate." + split[0], { + return I18n.t("topics.none.educate." + tab, { userPrefsUrl: userPath(`${this.currentUser.get('username_lower')}/preferences`) }); }.property('allLoaded', 'model.topics.length') From a64cc9a9900ad4d6dc5d003b1c625b34bd8f16e5 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Thu, 29 Mar 2018 10:53:57 +0200 Subject: [PATCH 076/287] FEATURE: allow users to collapse profile after expanding it --- .../javascripts/discourse/controllers/user.js.es6 | 4 ++-- app/assets/javascripts/discourse/templates/user.hbs | 13 +++++++++---- config/locales/client.en.yml | 1 + 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/discourse/controllers/user.js.es6 b/app/assets/javascripts/discourse/controllers/user.js.es6 index c8fce33d5a..59b42d13f4 100644 --- a/app/assets/javascripts/discourse/controllers/user.js.es6 +++ b/app/assets/javascripts/discourse/controllers/user.js.es6 @@ -91,8 +91,8 @@ export default Ember.Controller.extend(CanCheckEmails, { }, actions: { - expandProfile() { - this.set('forceExpand', true); + toggleExtendedProfile() { + this.toggleProperty('forceExpand'); }, showSuspensions() { diff --git a/app/assets/javascripts/discourse/templates/user.hbs b/app/assets/javascripts/discourse/templates/user.hbs index 469dc53953..9360db6536 100644 --- a/app/assets/javascripts/discourse/templates/user.hbs +++ b/app/assets/javascripts/discourse/templates/user.hbs @@ -67,12 +67,17 @@ connectorTagName="li" args=(hash model=model)}} - {{#if collapsedInfo}} {{#if viewingSelf}} -

  • {{d-icon "angle-double-down"}}{{i18n 'user.expand_profile'}}
  • +
  • + + {{#if collapsedInfo}} + {{d-icon "angle-double-down"}} {{i18n 'user.expand_profile'}} + {{else}} + {{d-icon "angle-double-up"}} {{i18n 'user.collapse_profile'}} + {{/if}} + +
  • {{/if}} - {{/if}} - diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 52bf9d2469..ebd4ee5346 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -617,6 +617,7 @@ en: activity_stream: "Activity" preferences: "Preferences" expand_profile: "Expand" + collapse_profile: "Collapse" bookmarks: "Bookmarks" bio: "About me" invited_by: "Invited By" From 27f06505b1814209b8a712d89534237dc0bc57a1 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 29 Mar 2018 10:25:29 +0800 Subject: [PATCH 077/287] Allow placeholder to be configured for `combo-box`. --- .../select-kit/components/combo-box.js.es6 | 3 +++ .../components/combo-box/combo-box-header.js.es6 | 3 ++- .../components/combo-box/combo-box-header.hbs | 12 +++++++++--- .../stylesheets/common/select-kit/combo-box.scss | 3 +++ 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/select-kit/components/combo-box.js.es6 b/app/assets/javascripts/select-kit/components/combo-box.js.es6 index e3bba9697c..4f22916d50 100644 --- a/app/assets/javascripts/select-kit/components/combo-box.js.es6 +++ b/app/assets/javascripts/select-kit/components/combo-box.js.es6 @@ -24,8 +24,11 @@ export default SingleSelectComponent.extend({ @on("didReceiveAttrs") _setComboBoxOptions() { + const placeholder = this.get('placeholder'); + this.get("headerComponentOptions").setProperties({ clearable: this.get("clearable"), + placeholder: placeholder ? I18n.t(placeholder) : "", }); } }); diff --git a/app/assets/javascripts/select-kit/components/combo-box/combo-box-header.js.es6 b/app/assets/javascripts/select-kit/components/combo-box/combo-box-header.js.es6 index 3835dc01cb..3777d6caa9 100644 --- a/app/assets/javascripts/select-kit/components/combo-box/combo-box-header.js.es6 +++ b/app/assets/javascripts/select-kit/components/combo-box/combo-box-header.js.es6 @@ -7,5 +7,6 @@ export default SelectKitHeaderComponent.extend({ clearable: Ember.computed.alias("options.clearable"), caretUpIcon: Ember.computed.alias("options.caretUpIcon"), caretDownIcon: Ember.computed.alias("options.caretDownIcon"), - shouldDisplayClearableButton: Ember.computed.and("clearable", "computedContent.hasSelection") + shouldDisplayClearableButton: Ember.computed.and("clearable", "computedContent.hasSelection"), + placeholder: Ember.computed.alias("options.placeholder"), }); diff --git a/app/assets/javascripts/select-kit/templates/components/combo-box/combo-box-header.hbs b/app/assets/javascripts/select-kit/templates/components/combo-box/combo-box-header.hbs index c3a7aa14fb..7a4f405595 100644 --- a/app/assets/javascripts/select-kit/templates/components/combo-box/combo-box-header.hbs +++ b/app/assets/javascripts/select-kit/templates/components/combo-box/combo-box-header.hbs @@ -1,8 +1,14 @@ {{#each icons as |icon|}} {{d-icon icon}} {{/each}} - - {{{label}}} - +{{#if label}} + + {{{label}}} + +{{else}} + + {{placeholder}} + +{{/if}} {{#if shouldDisplayClearableButton}}