├── .discourse-compatibility ├── .github └── workflows │ └── discourse-plugin.yml ├── .gitignore ├── .npmrc ├── .prettierrc.cjs ├── .rubocop.yml ├── .streerc ├── .template-lintrc.cjs ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.md ├── app ├── controllers │ ├── webhooks_controller.rb │ └── webinars_controller.rb ├── jobs │ └── scheduled │ │ └── send_webinar_reminders.rb ├── models │ ├── webinar.rb │ ├── webinar_user.rb │ └── zoom_webinar_webhook_event.rb ├── serializers │ ├── host_serializer.rb │ └── webinar_serializer.rb ├── services │ └── problem_check │ │ └── s2s_webinar_subscription.rb └── views │ └── zoom │ └── webinars │ └── sdk.erb ├── assets ├── javascripts │ └── discourse │ │ ├── adapters │ │ └── webinar.js │ │ ├── api-initializers │ │ └── zoom-composer.gjs │ │ ├── components │ │ ├── modal │ │ │ ├── edit-webinar.gjs │ │ │ └── webinar-picker.gjs │ │ ├── post-top-webinar.gjs │ │ ├── remove-webinar-from-composer.gjs │ │ ├── webinar-option-row.gjs │ │ ├── webinar-register.gjs │ │ └── webinar.gjs │ │ ├── connectors │ │ ├── composer-fields │ │ │ └── remove-webinar.gjs │ │ ├── editor-preview │ │ │ └── webinar.gjs │ │ └── user-activity-bottom │ │ │ └── webinars-list.gjs │ │ ├── initializers │ │ ├── admin-menu-webinar-button.js │ │ └── composer-toolbar-webinar-button.js │ │ ├── lib │ │ └── webinar-helpers.js │ │ ├── routes │ │ └── user-activity-webinars.js │ │ └── webinars-route-map.js └── stylesheets │ └── common │ ├── webinar-details.scss │ ├── webinar-picker.scss │ └── zoom.scss ├── config ├── locales │ ├── client.ar.yml │ ├── client.be.yml │ ├── client.bg.yml │ ├── client.bs_BA.yml │ ├── client.ca.yml │ ├── client.cs.yml │ ├── client.da.yml │ ├── client.de.yml │ ├── client.el.yml │ ├── client.en.yml │ ├── client.en_GB.yml │ ├── client.es.yml │ ├── client.et.yml │ ├── client.fa_IR.yml │ ├── client.fi.yml │ ├── client.fr.yml │ ├── client.gl.yml │ ├── client.he.yml │ ├── client.hr.yml │ ├── client.hu.yml │ ├── client.hy.yml │ ├── client.id.yml │ ├── client.it.yml │ ├── client.ja.yml │ ├── client.ko.yml │ ├── client.lt.yml │ ├── client.lv.yml │ ├── client.nb_NO.yml │ ├── client.nl.yml │ ├── client.pl_PL.yml │ ├── client.pt.yml │ ├── client.pt_BR.yml │ ├── client.ro.yml │ ├── client.ru.yml │ ├── client.sk.yml │ ├── client.sl.yml │ ├── client.sq.yml │ ├── client.sr.yml │ ├── client.sv.yml │ ├── client.sw.yml │ ├── client.te.yml │ ├── client.th.yml │ ├── client.tr_TR.yml │ ├── client.ug.yml │ ├── client.uk.yml │ ├── client.ur.yml │ ├── client.vi.yml │ ├── client.zh_CN.yml │ ├── client.zh_TW.yml │ ├── server.ar.yml │ ├── server.be.yml │ ├── server.bg.yml │ ├── server.bs_BA.yml │ ├── server.ca.yml │ ├── server.cs.yml │ ├── server.da.yml │ ├── server.de.yml │ ├── server.el.yml │ ├── server.en.yml │ ├── server.en_GB.yml │ ├── server.es.yml │ ├── server.et.yml │ ├── server.fa_IR.yml │ ├── server.fi.yml │ ├── server.fr.yml │ ├── server.gl.yml │ ├── server.he.yml │ ├── server.hr.yml │ ├── server.hu.yml │ ├── server.hy.yml │ ├── server.id.yml │ ├── server.it.yml │ ├── server.ja.yml │ ├── server.ko.yml │ ├── server.lt.yml │ ├── server.lv.yml │ ├── server.nb_NO.yml │ ├── server.nl.yml │ ├── server.pl_PL.yml │ ├── server.pt.yml │ ├── server.pt_BR.yml │ ├── server.ro.yml │ ├── server.ru.yml │ ├── server.sk.yml │ ├── server.sl.yml │ ├── server.sq.yml │ ├── server.sr.yml │ ├── server.sv.yml │ ├── server.sw.yml │ ├── server.te.yml │ ├── server.th.yml │ ├── server.tr_TR.yml │ ├── server.ug.yml │ ├── server.uk.yml │ ├── server.ur.yml │ ├── server.vi.yml │ ├── server.zh_CN.yml │ └── server.zh_TW.yml └── settings.yml ├── db └── migrate │ ├── 20191216205611_create_webinars.rb │ ├── 20191218154551_create_webinar_users.rb │ ├── 20191219200040_add_webinar_attributes.rb │ ├── 20200103155524_add_webinar_settings.rb │ ├── 20200103190818_add_registration_status_to_webinar_users.rb │ ├── 20200108204521_create_zoom_webhooks.rb │ ├── 20200113155455_add_status_to_webinar.rb │ ├── 20200114171405_drop_webinar_users_registration_status.rb │ ├── 20200117165426_add_video_url_to_webinars.rb │ ├── 20200122200143_add_reminder_sent_at_to_webinars.rb │ └── 20241009162757_alter_webinar_id_to_bigint.rb ├── eslint.config.mjs ├── lib ├── client.rb ├── oauth_client.rb ├── webinar_creator.rb ├── webinars.rb └── zoom │ ├── topic_extension.rb │ └── user_extension.rb ├── package.json ├── plugin.rb ├── pnpm-lock.yaml ├── public └── javascripts │ └── webinar-join.js ├── spec ├── fabricators │ └── webinar_fabricator.rb ├── jobs │ └── remind_webinar_attendees_spec.rb ├── lib │ ├── post_creator_spec.rb │ ├── webinar_creator_spec.rb │ └── zoom_client_spec.rb ├── models │ └── webinar_spec.rb ├── plugin_helper.rb ├── requests │ ├── oauth_spec.rb │ ├── webhooks_controller_spec.rb │ └── webinars_controller_spec.rb ├── responses │ └── zoom_api_stubs.rb └── system │ ├── core_features_spec.rb │ └── topic_page_spec.rb ├── stylelint.config.mjs ├── test └── javascripts │ └── integration │ └── components │ └── webinar-register-test.gjs └── translator.yml /.discourse-compatibility: -------------------------------------------------------------------------------- 1 | < 3.6.0.beta1-dev: bfcd1b45222e1d151710a2550374b1b4d6624163 2 | < 3.5.0.beta8-dev: 70e7062dc2a1db17fea27035c8bb5391219330dd 3 | < 3.5.0.beta5-dev: a3333652c47fd069c4094dfa2d4ca0ed38a8778c 4 | < 3.5.0.beta1-dev: fa73525857d66e086f07bf38390212479add103e 5 | < 3.4.0.beta2-dev: 7b1715ea437caf5ef8620c3b7e3446f0318c5f2c 6 | < 3.4.0.beta1-dev: 57660d995c8b31645eb37ab7188100a13c6a0c64 7 | < 3.3.0.beta2-dev: 4c63b225936f4f63d9f7b532864e43909de6293d 8 | < 3.3.0.beta1-dev: 1c4c1ba7c2821688176d7ee51022ea8ebb7f60bd 9 | < 3.2.0.beta2: 6f5f00f7ee610da254b356ad11e9f3665235dcd6 10 | 3.1.999: d5ff8a7832ace8355e60a427e2269e81575a4707 11 | 2.7.0.beta3: 81120be3a7602834a18e93ddbd1a69bbf7ebb608 12 | -------------------------------------------------------------------------------- /.github/workflows/discourse-plugin.yml: -------------------------------------------------------------------------------- 1 | name: Discourse Plugin 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | ci: 11 | uses: discourse/.github/.github/workflows/discourse-plugin.yml@v1 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /gems 3 | /auto_generated 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict = true 2 | auto-install-peers = false 3 | -------------------------------------------------------------------------------- /.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = require("@discourse/lint-configs/prettier"); 2 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_gem: 2 | rubocop-discourse: stree-compat.yml 3 | -------------------------------------------------------------------------------- /.streerc: -------------------------------------------------------------------------------- 1 | --print-width=100 2 | --plugins=plugin/trailing_comma,plugin/disable_auto_ternary 3 | -------------------------------------------------------------------------------- /.template-lintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = require("@discourse/lint-configs/template-lint"); 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | group :development do 6 | gem "rubocop-discourse" 7 | gem "syntax_tree" 8 | end 9 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | activesupport (8.0.3) 5 | base64 6 | benchmark (>= 0.3) 7 | bigdecimal 8 | concurrent-ruby (~> 1.0, >= 1.3.1) 9 | connection_pool (>= 2.2.5) 10 | drb 11 | i18n (>= 1.6, < 2) 12 | logger (>= 1.4.2) 13 | minitest (>= 5.1) 14 | securerandom (>= 0.3) 15 | tzinfo (~> 2.0, >= 2.0.5) 16 | uri (>= 0.13.1) 17 | ast (2.4.3) 18 | base64 (0.3.0) 19 | benchmark (0.4.1) 20 | bigdecimal (3.3.0) 21 | concurrent-ruby (1.3.5) 22 | connection_pool (2.5.4) 23 | drb (2.2.3) 24 | i18n (1.14.7) 25 | concurrent-ruby (~> 1.0) 26 | json (2.15.1) 27 | language_server-protocol (3.17.0.5) 28 | lint_roller (1.1.0) 29 | logger (1.7.0) 30 | minitest (5.26.0) 31 | parallel (1.27.0) 32 | parser (3.3.9.0) 33 | ast (~> 2.4.1) 34 | racc 35 | prettier_print (1.2.1) 36 | prism (1.5.1) 37 | racc (1.8.1) 38 | rack (3.2.3) 39 | rainbow (3.1.1) 40 | regexp_parser (2.11.3) 41 | rubocop (1.81.1) 42 | json (~> 2.3) 43 | language_server-protocol (~> 3.17.0.2) 44 | lint_roller (~> 1.1.0) 45 | parallel (~> 1.10) 46 | parser (>= 3.3.0.2) 47 | rainbow (>= 2.2.2, < 4.0) 48 | regexp_parser (>= 2.9.3, < 3.0) 49 | rubocop-ast (>= 1.47.1, < 2.0) 50 | ruby-progressbar (~> 1.7) 51 | unicode-display_width (>= 2.4.0, < 4.0) 52 | rubocop-ast (1.47.1) 53 | parser (>= 3.3.7.2) 54 | prism (~> 1.4) 55 | rubocop-capybara (2.22.1) 56 | lint_roller (~> 1.1) 57 | rubocop (~> 1.72, >= 1.72.1) 58 | rubocop-discourse (3.13.3) 59 | activesupport (>= 6.1) 60 | lint_roller (>= 1.1.0) 61 | rubocop (>= 1.73.2) 62 | rubocop-capybara (>= 2.22.0) 63 | rubocop-factory_bot (>= 2.27.0) 64 | rubocop-rails (>= 2.30.3) 65 | rubocop-rspec (>= 3.0.1) 66 | rubocop-rspec_rails (>= 2.31.0) 67 | rubocop-factory_bot (2.27.1) 68 | lint_roller (~> 1.1) 69 | rubocop (~> 1.72, >= 1.72.1) 70 | rubocop-rails (2.33.4) 71 | activesupport (>= 4.2.0) 72 | lint_roller (~> 1.1) 73 | rack (>= 1.1) 74 | rubocop (>= 1.75.0, < 2.0) 75 | rubocop-ast (>= 1.44.0, < 2.0) 76 | rubocop-rspec (3.7.0) 77 | lint_roller (~> 1.1) 78 | rubocop (~> 1.72, >= 1.72.1) 79 | rubocop-rspec_rails (2.31.0) 80 | lint_roller (~> 1.1) 81 | rubocop (~> 1.72, >= 1.72.1) 82 | rubocop-rspec (~> 3.5) 83 | ruby-progressbar (1.13.0) 84 | securerandom (0.4.1) 85 | syntax_tree (6.3.0) 86 | prettier_print (>= 1.2.0) 87 | tzinfo (2.0.6) 88 | concurrent-ruby (~> 1.0) 89 | unicode-display_width (3.2.0) 90 | unicode-emoji (~> 4.1) 91 | unicode-emoji (4.1.0) 92 | uri (1.0.4) 93 | 94 | PLATFORMS 95 | ruby 96 | 97 | DEPENDENCIES 98 | rubocop-discourse 99 | syntax_tree 100 | 101 | BUNDLED WITH 102 | 2.7.2 103 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Civilized Discourse Construction Kit, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Discourse Zoom Plugin 2 | 3 | This plugin integrates Zoom webinars into a Discourse instance. 4 | 5 | For more information, please see: https://meta.discourse.org/t/zoom-webinars-plugin/142711 6 | -------------------------------------------------------------------------------- /app/controllers/webhooks_controller.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Zoom 3 | class WebhooksController < ApplicationController 4 | requires_plugin ::Zoom::PLUGIN_NAME 5 | 6 | skip_before_action :verify_authenticity_token, :redirect_to_login_if_required 7 | before_action :filter_unhandled, :ensure_webhook_authenticity, :filter_expired_event 8 | 9 | HANDLED_EVENTS = %w[ 10 | webinar.updated 11 | webinar.started 12 | webinar.ended 13 | webinar.participant_joined 14 | webinar.participant_left 15 | endpoint.url_validation 16 | ] 17 | 18 | def webinars 19 | request_params = webinar_params 20 | if request_params[:event] == "endpoint.url_validation" 21 | secret = SiteSetting.zoom_webhooks_secret_token 22 | encrypted_token = 23 | OpenSSL::HMAC.hexdigest( 24 | OpenSSL::Digest.new("sha256"), 25 | secret, 26 | JSON.parse(request.body.read, symbolize_names: true)[:payload][:plainToken], 27 | ) 28 | return( 29 | render json: { 30 | plainToken: request_params[:payload][:plain_token], 31 | encryptedToken: encrypted_token, 32 | }, 33 | status: :ok 34 | ) 35 | else 36 | send(handler_for(request_params[:event])) 37 | end 38 | 39 | render json: success_json 40 | end 41 | 42 | private 43 | 44 | def handler_for(event) 45 | event.gsub(".", "_").to_sym 46 | end 47 | 48 | def webinar_updated 49 | raise Discourse::NotFound unless old_webinar 50 | 51 | old_webinar.update_from_zoom(webinar_params.dig(:payload, :object)) 52 | end 53 | 54 | def webinar_started 55 | raise Discourse::NotFound unless webinar 56 | 57 | webinar.update(status: :started) 58 | end 59 | 60 | def webinar_ended 61 | raise Discourse::NotFound unless webinar 62 | 63 | webinar.update(status: :ended) 64 | end 65 | 66 | def webinar_participant_joined 67 | DiscourseEvent.trigger(:webinar_participant_joined, webinar, webinar_params) 68 | end 69 | 70 | def webinar_participant_left 71 | DiscourseEvent.trigger(:webinar_participant_left, webinar, webinar_params) 72 | end 73 | 74 | def ensure_webhook_authenticity 75 | message = "v0:#{request.headers["x-zm-request-timestamp"]}:#{request.body.read}" 76 | 77 | secret = SiteSetting.zoom_webhooks_secret_token 78 | 79 | calculated_hash = OpenSSL::HMAC.hexdigest("SHA256", secret, message) 80 | signature = "v0=#{calculated_hash}" 81 | request_signature = request.headers["x-zm-signature"] 82 | 83 | if !ActiveSupport::SecurityUtils.secure_compare(signature, request_signature) 84 | raise Discourse::InvalidAccess.new 85 | end 86 | end 87 | 88 | def user 89 | @user ||= 90 | begin 91 | user = User.find_by_email(registrant[:email]) 92 | return user if user 93 | 94 | stage_user 95 | end 96 | end 97 | 98 | def stage_user 99 | User.create!( 100 | email: registrant[:email], 101 | username: UserNameSuggester.suggest(registrant[:email]), 102 | name: User.suggest_name(registrant[:email]), 103 | staged: true, 104 | ) 105 | end 106 | 107 | def filter_unhandled 108 | raise Discourse::NotFound if HANDLED_EVENTS.exclude?(webinar_params[:event]) 109 | end 110 | 111 | def filter_expired_event 112 | payload_data = webinar_params[:payload].to_h 113 | payload = MultiJson.dump(payload_data) 114 | 115 | new_event = 116 | ::ZoomWebinarWebhookEvent.new( 117 | event: webinar_params[:event], 118 | payload: payload, 119 | webinar_id: payload_data.dig(:object, :id)&.to_i, 120 | zoom_timestamp: payload_data[:time_stamp]&.to_i, 121 | ) 122 | 123 | if new_event.zoom_timestamp 124 | later_events = 125 | ::ZoomWebinarWebhookEvent.where( 126 | %Q(event = '#{new_event.event}' 127 | AND webinar_id = #{new_event.webinar_id} 128 | AND zoom_timestamp >= #{new_event.zoom_timestamp}), 129 | ) 130 | raise Discourse::NotFound if later_events.any? 131 | new_event.save! 132 | end 133 | 134 | true 135 | end 136 | 137 | def old_webinar 138 | @old_weninar ||= find_webinar_from(:old_object) 139 | end 140 | 141 | def webinar 142 | @weninar ||= find_webinar_from(:object) 143 | end 144 | 145 | def find_webinar_from(key) 146 | zoom_id = webinar_params.fetch(:payload, {}).fetch(key, {}).fetch(:id, {}) 147 | return nil unless zoom_id 148 | 149 | Webinar.find_by(zoom_id: zoom_id) 150 | end 151 | 152 | def registrant 153 | @registrant ||= webinar_params.fetch(:payload, {}).fetch(:object, {}).fetch(:registrant, {}) 154 | end 155 | 156 | def webinar_params 157 | params.require(:webhook).permit(:event, :event_ts, payload: {}) 158 | end 159 | end 160 | end 161 | -------------------------------------------------------------------------------- /app/jobs/scheduled/send_webinar_reminders.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Jobs 4 | class ::Zoom::SendWebinarReminders < ::Jobs::Scheduled 5 | every 5.minutes 6 | 7 | def execute(args) 8 | reminder_time = SiteSetting.zoom_send_reminder_minutes_before_webinar&.to_i 9 | return if reminder_time < 0 10 | 11 | webinars = 12 | Webinar 13 | .where( 14 | "starts_at > ? AND starts_at < ? AND reminders_sent_at IS NULL", 15 | DateTime.now, 16 | DateTime.now + reminder_time.minutes + 2.5.minutes, 17 | ) 18 | .each do |webinar| 19 | webinar.webinar_users.each do |webinar_user| 20 | ::Jobs.enqueue( 21 | :send_system_message, 22 | user_id: webinar_user.user_id, 23 | message_type: "webinar_reminder", 24 | message_options: { 25 | url: webinar.topic.url, 26 | }, 27 | ) 28 | end 29 | webinar.update(reminders_sent_at: DateTime.now) 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /app/models/webinar.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Webinar < ActiveRecord::Base 4 | enum :approval_type, { automatic: 0, manual: 1, no_registration: 2 } 5 | enum :status, { pending: 0, started: 1, ended: 2 } 6 | 7 | has_many :webinar_users 8 | has_many :users, through: :webinar_users 9 | belongs_to :topic 10 | 11 | validates :zoom_id, presence: true 12 | validates :zoom_id, uniqueness: { message: :webinar_in_use }, unless: :non_zoom_event? 13 | 14 | validates :topic_id, presence: true 15 | 16 | after_commit :notify_status_update, on: :update 17 | 18 | ZOOM_ATTRIBUTE_MAP = { id: :zoom_id, topic: :title, start_time: :starts_at }.freeze 19 | 20 | def self.sanitize_zoom_id(dirty_id) 21 | dirty_id.to_s.strip.gsub("-", "") 22 | end 23 | 24 | def attendees 25 | users.joins(:webinar_users).where("webinar_users.type = #{WebinarUser.types[:attendee]}").uniq 26 | end 27 | 28 | def panelists 29 | users.joins(:webinar_users).where("webinar_users.type = #{WebinarUser.types[:panelist]}").uniq 30 | end 31 | 32 | def host 33 | users.joins(:webinar_users).where("webinar_users.type = #{WebinarUser.types[:host]}").first 34 | end 35 | 36 | def update_from_zoom(zoom_attributes) 37 | update(convert_attributes_from_zoom(zoom_attributes)) 38 | end 39 | 40 | def convert_attributes_from_zoom(zoom_attributes) 41 | zoom_attributes = 42 | (zoom_attributes[:settings] || {}) 43 | .merge(zoom_attributes.except(:settings)) 44 | .to_h 45 | .deep_symbolize_keys 46 | 47 | zoom_attributes[:approval_type] = zoom_attributes[:approval_type].to_i if zoom_attributes[ 48 | :approval_type 49 | ] 50 | zoom_attributes[:zoom_host_id] = zoom_attributes[:host_id] 51 | if zoom_attributes[:start_time] || zoom_attributes[:duration] 52 | zoom_attributes[:start_time] = zoom_attributes[:start_time] || starts_at.to_s 53 | zoom_attributes[:duration] = zoom_attributes[:duration] || duration 54 | zoom_attributes[:ends_at] = ( 55 | DateTime.parse(zoom_attributes[:start_time]) + zoom_attributes[:duration].to_i.minutes 56 | ).to_s 57 | end 58 | 59 | converted_attributes = {} 60 | 61 | zoom_attributes.each do |key, value| 62 | converted_key = ZOOM_ATTRIBUTE_MAP[key] || key 63 | converted_attributes[converted_key] = value if has_attribute? converted_key 64 | end 65 | converted_attributes 66 | end 67 | 68 | def non_zoom_event? 69 | zoom_id == "nonzoom" 70 | end 71 | 72 | private 73 | 74 | def notify_status_update 75 | return if previous_changes["status"].nil? 76 | 77 | MessageBus.publish("/zoom/webinars/#{id}", status: status) 78 | end 79 | end 80 | 81 | # == Schema Information 82 | # 83 | # Table name: webinars 84 | # 85 | # id :bigint not null, primary key 86 | # topic_id :integer 87 | # zoom_id :string 88 | # title :string 89 | # starts_at :datetime 90 | # ends_at :datetime 91 | # duration :integer 92 | # zoom_host_id :string 93 | # created_at :datetime not null 94 | # updated_at :datetime not null 95 | # host_video :boolean 96 | # panelists_video :boolean 97 | # approval_type :integer default("no_registration"), not null 98 | # enforce_login :boolean 99 | # registrants_restrict_number :integer default(0), not null 100 | # meeting_authentication :boolean 101 | # on_demand :boolean 102 | # join_url :string 103 | # password :string 104 | # status :integer default("pending"), not null 105 | # video_url :text 106 | # reminders_sent_at :datetime 107 | # 108 | -------------------------------------------------------------------------------- /app/models/webinar_user.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | class WebinarUser < ActiveRecord::Base 3 | self.inheritance_column = :_type_disabled 4 | 5 | enum :type, { attendee: 0, panelist: 1, host: 2 } 6 | 7 | validates :type, presence: true, inclusion: { in: types.keys } 8 | validates :webinar_id, presence: true 9 | validates :user_id, 10 | presence: true, 11 | uniqueness: { 12 | scope: :webinar_id, 13 | message: "user can only be registered once", 14 | } 15 | 16 | belongs_to :user 17 | belongs_to :webinar 18 | end 19 | 20 | # == Schema Information 21 | # 22 | # Table name: webinar_users 23 | # 24 | # id :bigint not null, primary key 25 | # user_id :integer 26 | # webinar_id :bigint 27 | # type :integer 28 | # 29 | -------------------------------------------------------------------------------- /app/models/zoom_webinar_webhook_event.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ZoomWebinarWebhookEvent < ActiveRecord::Base 4 | end 5 | 6 | # == Schema Information 7 | # 8 | # Table name: zoom_webinar_webhook_events 9 | # 10 | # id :bigint not null, primary key 11 | # event :string 12 | # payload :text 13 | # webinar_id :integer 14 | # zoom_timestamp :bigint 15 | # created_at :datetime not null 16 | # updated_at :datetime not null 17 | # 18 | -------------------------------------------------------------------------------- /app/serializers/host_serializer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class HostSerializer < BasicUserSerializer 4 | attributes :title, :avatar_template 5 | 6 | def title 7 | if SiteSetting.zoom_host_title_override 8 | field_id = UserField.where(name: SiteSetting.zoom_host_title_override).pluck(:id).first 9 | return object.user_fields[field_id.to_s] || "" 10 | end 11 | 12 | object.title 13 | end 14 | 15 | def avatar_template 16 | User.avatar_template(object.username, object.uploaded_avatar_id) 17 | end 18 | 19 | def include_avatar_template? 20 | true 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /app/serializers/webinar_serializer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class WebinarSerializer < ApplicationSerializer 4 | has_one :host, serializer: HostSerializer, embed: :objects 5 | has_many :attendees, serializer: BasicUserSerializer, embed: :objects 6 | has_many :panelists, serializer: BasicUserSerializer, embed: :objects 7 | 8 | attributes :topic_id, 9 | :id, 10 | :zoom_id, 11 | :title, 12 | :starts_at, 13 | :ends_at, 14 | :duration, 15 | :zoom_host_id, 16 | :require_password, 17 | :host_video, 18 | :panelists_video, 19 | :approval_type, 20 | :enforce_login, 21 | :registrants_restrict_number, 22 | :meeting_authentication, 23 | :on_demand, 24 | :join_url, 25 | :status, 26 | :video_url 27 | 28 | def require_password 29 | object.password.present? 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /app/services/problem_check/s2s_webinar_subscription.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ProblemCheck::S2sWebinarSubscription < ProblemCheck::InlineProblemCheck 4 | self.priority = "high" 5 | end 6 | -------------------------------------------------------------------------------- /app/views/zoom/webinars/sdk.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 149 | -------------------------------------------------------------------------------- /assets/javascripts/discourse/adapters/webinar.js: -------------------------------------------------------------------------------- 1 | import RestAdapter from "discourse/adapters/rest"; 2 | 3 | export default class Webinar extends RestAdapter { 4 | basePath() { 5 | return "/zoom/"; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /assets/javascripts/discourse/api-initializers/zoom-composer.gjs: -------------------------------------------------------------------------------- 1 | import { apiInitializer } from "discourse/lib/api"; 2 | import { withSilencedDeprecations } from "discourse/lib/deprecated"; 3 | import Composer from "discourse/models/composer"; 4 | import RenderGlimmer from "discourse/widgets/render-glimmer"; 5 | import PostTopWebinar from "../components/post-top-webinar"; 6 | 7 | export default apiInitializer((api) => { 8 | Composer.serializeOnCreate("zoom_id", "zoomId"); 9 | Composer.serializeOnCreate("zoom_webinar_title", "zoomWebinarTitle"); 10 | Composer.serializeOnCreate("zoom_webinar_start_date", "zoomWebinarStartDate"); 11 | 12 | customizePost(api); 13 | }); 14 | 15 | function customizePost(api) { 16 | api.renderBeforeWrapperOutlet( 17 | "post-article", 18 | 19 | ); 20 | 21 | withSilencedDeprecations("discourse.post-stream-widget-overrides", () => 22 | customizeWidgetPost(api) 23 | ); 24 | } 25 | 26 | function customizeWidgetPost(api) { 27 | const PostBeforeComponent = ; 30 | 31 | api.decorateWidget("post:before", (dec) => { 32 | if (dec.attrs.firstPost && !dec.attrs.cloaked) { 33 | const post = dec.widget.findAncestorModel(); 34 | return new RenderGlimmer( 35 | dec.widget, 36 | "div.widget-connector", 37 | PostBeforeComponent, 38 | { post } 39 | ); 40 | } 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /assets/javascripts/discourse/components/post-top-webinar.gjs: -------------------------------------------------------------------------------- 1 | import bodyClass from "discourse/helpers/body-class"; 2 | import Webinar from "./webinar"; 3 | 4 | const PostTopWebinar = ; 12 | 13 | export default PostTopWebinar; 14 | -------------------------------------------------------------------------------- /assets/javascripts/discourse/components/remove-webinar-from-composer.gjs: -------------------------------------------------------------------------------- 1 | import Component from "@ember/component"; 2 | import { action } from "@ember/object"; 3 | import DButton from "discourse/components/d-button"; 4 | 5 | export default class RemoveWebinarFromComposer extends Component { 6 | model = null; 7 | 8 | @action 9 | removeWebinar() { 10 | this.model.set("zoomId", null); 11 | } 12 | 13 | 31 | } 32 | -------------------------------------------------------------------------------- /assets/javascripts/discourse/components/webinar-option-row.gjs: -------------------------------------------------------------------------------- 1 | import Component from "@ember/component"; 2 | import { on } from "@ember/modifier"; 3 | import { action } from "@ember/object"; 4 | import discourseComputed from "discourse/lib/decorators"; 5 | import { formattedSchedule } from "../lib/webinar-helpers"; 6 | 7 | export default class WebinarOptionRow extends Component { 8 | model = null; 9 | onSelect = null; 10 | 11 | init() { 12 | super.init(...arguments); 13 | this.onSelect = this.onSelect || (() => {}); 14 | } 15 | 16 | @discourseComputed("model") 17 | schedule(model) { 18 | return formattedSchedule( 19 | model.start_time, 20 | moment(model.start_time).add(model.duration, "m").toDate() 21 | ); 22 | } 23 | 24 | @action 25 | selectWebinar(event) { 26 | event.preventDefault(); 27 | this.onSelect(); 28 | } 29 | 30 | 46 | } 47 | -------------------------------------------------------------------------------- /assets/javascripts/discourse/connectors/composer-fields/remove-webinar.gjs: -------------------------------------------------------------------------------- 1 | import Component from "@ember/component"; 2 | import { classNames, tagName } from "@ember-decorators/component"; 3 | import RemoveWebinarFromComposer from "../../components/remove-webinar-from-composer"; 4 | 5 | @tagName("div") 6 | @classNames("composer-fields-outlet", "remove-webinar") 7 | export default class RemoveWebinar extends Component { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /assets/javascripts/discourse/connectors/editor-preview/webinar.gjs: -------------------------------------------------------------------------------- 1 | import Component from "@ember/component"; 2 | import { classNames, tagName } from "@ember-decorators/component"; 3 | import Webinar0 from "../../components/webinar"; 4 | 5 | @tagName("div") 6 | @classNames("editor-preview-outlet", "webinar") 7 | export default class Webinar extends Component { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /assets/javascripts/discourse/connectors/user-activity-bottom/webinars-list.gjs: -------------------------------------------------------------------------------- 1 | import Component from "@ember/component"; 2 | import { LinkTo } from "@ember/routing"; 3 | import { classNames, tagName } from "@ember-decorators/component"; 4 | import { i18n } from "discourse-i18n"; 5 | 6 | @tagName("") 7 | @classNames("user-activity-bottom-outlet", "webinars-list") 8 | export default class WebinarsList extends Component { 9 | 14 | } 15 | -------------------------------------------------------------------------------- /assets/javascripts/discourse/initializers/admin-menu-webinar-button.js: -------------------------------------------------------------------------------- 1 | import { ajax } from "discourse/lib/ajax"; 2 | import { popupAjaxError } from "discourse/lib/ajax-error"; 3 | import { withPluginApi } from "discourse/lib/plugin-api"; 4 | import { i18n } from "discourse-i18n"; 5 | import WebinarPicker from "../components/modal/webinar-picker"; 6 | 7 | function initialize(api) { 8 | api.addTopicAdminMenuButton((topic) => { 9 | const canManageTopic = api.getCurrentUser()?.canManageTopic; 10 | 11 | if (!topic.isPrivateMessage && canManageTopic) { 12 | return { 13 | icon: "shield-alt", 14 | label: topic.get("webinar") 15 | ? "zoom.remove_webinar" 16 | : "zoom.add_webinar", 17 | action: () => { 18 | if (topic.get("webinar")) { 19 | const dialog = api.container.lookup("service:dialog"); 20 | const topicController = api.container.lookup("controller:topic"); 21 | removeWebinar(topic, dialog, topicController); 22 | } else { 23 | const modal = api.container.lookup("service:modal"); 24 | showWebinarModal(topic, modal); 25 | } 26 | }, 27 | }; 28 | } 29 | }); 30 | } 31 | 32 | export default { 33 | name: "admin-menu-webinar-button", 34 | 35 | initialize() { 36 | withPluginApi(initialize); 37 | }, 38 | }; 39 | 40 | function showWebinarModal(topic, modal) { 41 | topic.set("addToTopic", true); 42 | modal.show(WebinarPicker, { 43 | model: { 44 | topic, 45 | setWebinar: (value) => topic.set("webinar", value), 46 | setZoomId: (value) => topic.set("zoomId", value), 47 | setWebinarTitle: (value) => topic.set("zoomWebinarTitle", value), 48 | setWebinarStartDate: (value) => topic.set("zoomWebinarStartDate", value), 49 | }, 50 | }); 51 | } 52 | 53 | function removeWebinar(topic, dialog, topicController) { 54 | dialog.confirm({ 55 | message: i18n("zoom.confirm_remove"), 56 | didConfirm: () => { 57 | ajax(`/zoom/webinars/${topic.webinar.id}`, { type: "DELETE" }) 58 | .then(() => { 59 | topic.set("webinar", null); 60 | topicController.set("editingTopic", false); 61 | document.body.classList.remove("has-webinar"); 62 | topic.postStream.posts[0].rebake(); 63 | }) 64 | .catch(popupAjaxError); 65 | }, 66 | }); 67 | } 68 | -------------------------------------------------------------------------------- /assets/javascripts/discourse/initializers/composer-toolbar-webinar-button.js: -------------------------------------------------------------------------------- 1 | import { withPluginApi } from "discourse/lib/plugin-api"; 2 | import WebinarPicker from "../components/modal/webinar-picker"; 3 | 4 | function initializeWebinarButton(api) { 5 | const modal = api.container.lookup("service:modal"); 6 | 7 | api.addComposerToolbarPopupMenuOption({ 8 | condition: (composer) => { 9 | return composer.model && composer.model.creatingTopic; 10 | }, 11 | icon: "video", 12 | label: "zoom.webinar_picker.button", 13 | action: () => { 14 | const composerService = api.container.lookup("service:composer"); 15 | 16 | modal.show(WebinarPicker, { 17 | model: { 18 | topic: composerService.model, 19 | setWebinar: (value) => composerService.model.set("webinar", value), 20 | setZoomId: (value) => composerService.model.set("zoomId", value), 21 | setWebinarTitle: (value) => 22 | composerService.model.set("zoomWebinarTitle", value), 23 | setWebinarStartDate: (value) => 24 | composerService.model.set("zoomWebinarStartDate", value), 25 | }, 26 | }); 27 | }, 28 | }); 29 | } 30 | 31 | export default { 32 | name: "composer-toolbar-webinar-button", 33 | 34 | initialize(container) { 35 | const siteSettings = container.lookup("service:site-settings"); 36 | const currentUser = container.lookup("service:current-user"); 37 | 38 | if (siteSettings.zoom_enabled && currentUser) { 39 | withPluginApi(initializeWebinarButton); 40 | } 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /assets/javascripts/discourse/lib/webinar-helpers.js: -------------------------------------------------------------------------------- 1 | export function formattedSchedule(start, end) { 2 | const startMoment = moment(start); 3 | const endMoment = moment(end); 4 | return `${startMoment.format("LT")} - ${endMoment.format( 5 | "LT" 6 | )}, ${startMoment.format("Do MMMM, Y")}`; 7 | } 8 | -------------------------------------------------------------------------------- /assets/javascripts/discourse/routes/user-activity-webinars.js: -------------------------------------------------------------------------------- 1 | import UserTopicListRoute from "discourse/routes/user-topic-list"; 2 | 3 | export default class UserActivityWebinars extends UserTopicListRoute { 4 | userActionType = null; 5 | noContentHelpKey = "zoom.no_user_webinars"; 6 | 7 | model() { 8 | return this.store.findFiltered("topicList", { 9 | filter: `topics/webinar-registrations/${this.modelFor("user").get( 10 | "username_lower" 11 | )}`, 12 | }); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /assets/javascripts/discourse/webinars-route-map.js: -------------------------------------------------------------------------------- 1 | export default { 2 | resource: "user.userActivity", 3 | 4 | map() { 5 | this.route("webinars"); 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /assets/stylesheets/common/webinar-details.scss: -------------------------------------------------------------------------------- 1 | .webinar-banner { 2 | max-width: calc(#{$topic-body-width} + (#{$topic-body-width-padding} * 2)); 3 | background: var(--primary-very-low); 4 | border: 1px solid var(--primary-low); 5 | border-radius: 5px; 6 | padding: 20px; 7 | margin-bottom: 20px; 8 | 9 | .edit-panelists { 10 | color: var(--primary); 11 | } 12 | } 13 | 14 | .webinar-header { 15 | margin-bottom: 1em; 16 | 17 | @media screen and (width >= 768px) { 18 | display: flex; 19 | justify-content: space-between; 20 | 21 | .webinar-title-section { 22 | align-self: center; 23 | } 24 | } 25 | 26 | .countdown-label { 27 | @media screen and (width >= 768px) { 28 | text-align: center; 29 | padding-bottom: 0.5em; 30 | } 31 | } 32 | 33 | @media screen and (width <= 767px) { 34 | .countdown-wrapper { 35 | margin: 1em 0; 36 | } 37 | } 38 | 39 | .countdown { 40 | display: flex; 41 | max-height: 75px; 42 | 43 | .pill { 44 | background: white; 45 | border: 1px solid var(--primary-low); 46 | margin: 0 5px; 47 | padding: 8px; 48 | border-radius: 5px; 49 | text-align: center; 50 | font-size: $font-up-3; 51 | 52 | > div { 53 | font-size: $font-down-4; 54 | } 55 | } 56 | } 57 | } 58 | 59 | .webinar-content { 60 | .webinar-title { 61 | font-size: $font-up-4; 62 | font-weight: bold; 63 | } 64 | 65 | h3 { 66 | margin-top: 0.5em; 67 | color: var(--primary); 68 | } 69 | 70 | .host { 71 | margin-top: 1em; 72 | } 73 | 74 | .host-container { 75 | margin-top: 0.5em; 76 | margin-bottom: 1.5em; 77 | display: flex; 78 | } 79 | 80 | .host-description { 81 | display: flex; 82 | flex-direction: column; 83 | justify-content: center; 84 | margin-left: 1.5em; 85 | } 86 | 87 | .panelists, 88 | .attendees { 89 | margin: 1em 0; 90 | display: flex; 91 | align-items: center; 92 | } 93 | 94 | .panelist-avatars, 95 | .attendee-avatars { 96 | margin-left: 10px; 97 | } 98 | 99 | .occurrence-start-time, 100 | .host-name, 101 | .host, 102 | .panelists { 103 | font-size: $base-font-size-larger; 104 | } 105 | 106 | .group-name { 107 | font-size: $base-font-size; 108 | } 109 | 110 | .event-ended { 111 | .video-recording { 112 | padding-top: 2em; 113 | 114 | video { 115 | width: 100%; 116 | } 117 | } 118 | } 119 | } 120 | 121 | .webinar-registered { 122 | margin-top: 1em; 123 | 124 | span.registered { 125 | display: inline-block; 126 | background: var(--secondary); 127 | border: 1px solid var(--primary-low); 128 | padding: 5px 15px; 129 | border-radius: 5px; 130 | } 131 | } 132 | 133 | .webinar-footnote { 134 | text-align: center; 135 | margin-top: 1.5em; 136 | font-size: $font-down-2; 137 | } 138 | -------------------------------------------------------------------------------- /assets/stylesheets/common/webinar-picker.scss: -------------------------------------------------------------------------------- 1 | .webinar-picker-webinars, 2 | .webinar-preview-input-group { 3 | .webinar-option { 4 | padding: 10px 5px; 5 | border-bottom: 1px solid var(--primary-low); 6 | 7 | .webinar-topic { 8 | font-size: $font-up-1; 9 | } 10 | 11 | .webinar-schedule, 12 | .webinar-id { 13 | color: var(--primary-medium); 14 | } 15 | } 16 | } 17 | 18 | .webinar-preview { 19 | margin: 20px; 20 | min-height: 250px; 21 | min-width: 280px; 22 | } 23 | 24 | .toggle-webinar-modal { 25 | margin-bottom: 7px; 26 | } 27 | 28 | .webinar-picker-add-past { 29 | padding: 1em 0; 30 | text-align: right; 31 | } 32 | 33 | .webinar-past-start-date { 34 | padding-bottom: 1em; 35 | } 36 | 37 | .webinar-picker-webinars { 38 | max-height: 300px; 39 | overflow: auto; 40 | } 41 | 42 | .webinar-picker-input { 43 | text-align: center; 44 | 45 | label { 46 | display: flex; 47 | flex-direction: column; 48 | font-size: var(--font-0); 49 | 50 | span { 51 | margin-bottom: 0.5rem; 52 | } 53 | } 54 | } 55 | 56 | .webinar-picker-wrapper { 57 | display: flex; 58 | justify-content: space-between; 59 | align-items: center; 60 | } 61 | -------------------------------------------------------------------------------- /assets/stylesheets/common/zoom.scss: -------------------------------------------------------------------------------- 1 | .composer-webinar { 2 | width: 50%; 3 | display: flex; 4 | align-items: baseline; 5 | justify-content: flex-start; 6 | 7 | .webinar-label { 8 | color: var(--primary-medium); 9 | } 10 | 11 | .webinar-title { 12 | font-size: 1rem; 13 | font-weight: normal; 14 | margin-left: 10px; 15 | } 16 | 17 | .cancel { 18 | order: 2; 19 | margin-left: 10px; 20 | } 21 | } 22 | 23 | #edit-webinar-modal { 24 | .webinar-panelist { 25 | display: flex; 26 | justify-content: space-between; 27 | line-height: 30px; 28 | padding: 3px 0; 29 | } 30 | 31 | .new-panelist-input, 32 | .new-panelist-btn { 33 | display: inline-block; 34 | vertical-align: middle; 35 | } 36 | 37 | .webinar-add-panelist, 38 | .webinar-add-video, 39 | .webinar-nonzoom-details + .webinar-panelists { 40 | margin-top: 1em; 41 | padding-top: 1em; 42 | border-top: 1px solid var(--primary-low); 43 | } 44 | 45 | .webinar-nonzoom-details .update-host-input { 46 | margin-bottom: 1em; 47 | } 48 | 49 | .update-host-details { 50 | display: flex; 51 | gap: 0.4em; 52 | align-items: center; 53 | 54 | input { 55 | margin-bottom: 0; 56 | } 57 | } 58 | } 59 | 60 | .webinar-registered { 61 | min-height: 35px; 62 | } 63 | 64 | @media screen and (width >= 768px) { 65 | .zoom-add-to-calendar-container { 66 | text-align: center; 67 | } 68 | 69 | .btn-flat + .zoom-add-to-calendar-container { 70 | text-align: right; 71 | float: right; 72 | } 73 | } 74 | 75 | @media screen and (width <= 767px) { 76 | .zoom-add-to-calendar-container { 77 | padding-top: 1em; 78 | max-width: 60vw; 79 | 80 | a { 81 | margin-bottom: 1em; 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /config/locales/client.ar.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | ar: 8 | admin_js: 9 | admin: 10 | site_settings: 11 | categories: 12 | discourse_zoom: "Discourse Zoom" 13 | site_settings: 14 | zoom_enabled: "تم تفعيل المكوِّن الإضافي لبرنامج Zoom" 15 | zoom_send_reminder_minutes_before_webinar: "قبل كم دقيقة من بدء الندوة عبر الإنترنت تريد إرسال إشعارات التذكير إلى الحاضرين؟ اضبط القيمة على \"فارغ\" أو 0 إذا كنت لا تريد إرسال الإشعارات." 16 | js: 17 | zoom: 18 | remove: "إزالة" 19 | remove_webinar: "إزالة الندوة عبر الإنترنت" 20 | edit_webinar: "تعديل الندوة عبر الإنترنت" 21 | add_webinar: "إضافة ندوة عبر الإنترنت" 22 | confirm_remove: "هل تريد بالتأكيد إزالة هذه الندوة عبر الإنترنت؟" 23 | toggle_edit_webinar_button: "تعديل الندوة عبر الإنترنت" 24 | webinar_picker: 25 | webinar_id: "معرِّف الندوة عبر الإنترنت" 26 | title: "إضافة ندوة عبر الإنترنت" 27 | create: "إضافة" 28 | button: "ندوة عبر الإنترنت على برنامج Zoom" 29 | clear: "اختر ندوة عبر الإنترنت مختلفة" 30 | register: "تسجيل" 31 | registered: "تم التسجيل" 32 | panelists: "المتحدثون" 33 | attendees: "الحاضرون" 34 | no_user_webinars: "أنت غير مسجَّل في أي ندوات عبر الإنترنت." 35 | webinars_title: "الندوات عبر الإنترنت" 36 | select_panelist: "اسم المستخدم أو البريد الإلكتروني" 37 | add_panelist: "إضافة متحدث" 38 | nonzoom_details: "البيانات الوصفية السابقة للندوة عبر الإنترنت" 39 | host: "المضيف" 40 | select_host: "اسم المستخدم أو البريد الإلكتروني" 41 | title_date: "العنوان والتاريخ" 42 | hosted_by: "المضيف:" 43 | countdown_label: "يبدأ الحدث بعد" 44 | days: "الأيام" 45 | hours: "الساعات" 46 | mins: "الدقائق" 47 | secs: "الثواني" 48 | no_panelists: "لا يوجد متحدثون مرتبطون بهذه الندوة عبر الإنترنت. استخدم النموذج أدناه لإضافة متحدث." 49 | no_panelists_preview: "لا يوجد متحدثون مرتبطون بهذه الندوة عبر الإنترنت. يمكنك إضافتهم لاحقًا." 50 | error: "حدث خطأ، يُرجى إعادة المحاولة. " 51 | join_sdk: "انضم الآن!" 52 | cancel_registration: "إلغاء التسجيل" 53 | no_registration_required: 'تم وضع علامة "مطلوب التسجيل" على هذه الندوة عبر الويب في برنامج Zoom، وهو غير مدعوم حاليًا. يُرجى تعديل إعدادات الندوة عبر الإنترنت وإعادة المحاولة.' 54 | webinar_existing_topic: "هذه الندوة عبر الإنترنت مرتبطة بالفعل بموضوع. معرِّف الموضوع: %{topic_id}" 55 | webinar_ended: "لقد انتهى الآن هذا الحدث." 56 | webinar_recording: "إضافة تسجيل للندوة عبر الإنترنت" 57 | webinar_recording_description: "أدخل عنوان URL لتسجيل الفيديو لهذه الندوة عبر الإنترنت. " 58 | show_recording: "اللعب مرة أخرى" 59 | add_to_google_calendar: "الإضافة إلى تقويم Google" 60 | add_to_outlook: "الإضافة إلى Outlook" 61 | add_to_calendar: "الإضافة إلى التقويم" 62 | webinar_footnote: "لا يمكنك حضور الحدث؟ سجِّل وسنُرسل إليك ملخصًا بعد الحدث." 63 | webinar_logged_in_users_only: "التسجيل في الندوة عبر الإنترنت متاح للمستخدمين الذين سجَّلوا الدخول فقط." 64 | add_past_webinar: "إضافة ندوة عبر الإنترنت سابقة" 65 | past_label: "عنوان الندوة عبر الإنترنت السابقة" 66 | past_date: "تاريخ الندوة عبر الإنترنت السابقة" 67 | -------------------------------------------------------------------------------- /config/locales/client.be.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | be: 8 | js: 9 | zoom: 10 | remove: "выдаліць" 11 | webinar_picker: 12 | create: "Дадаць" 13 | register: "рэгістрацыя" 14 | -------------------------------------------------------------------------------- /config/locales/client.bg.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | bg: 8 | js: 9 | zoom: 10 | remove: "Премахване" 11 | webinar_picker: 12 | create: "Добави " 13 | register: "Регистриране" 14 | hours: "Часа" 15 | -------------------------------------------------------------------------------- /config/locales/client.bs_BA.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | bs_BA: 8 | js: 9 | zoom: 10 | remove: "Ukloni" 11 | webinar_picker: 12 | create: "Add" 13 | register: "Registriraj" 14 | -------------------------------------------------------------------------------- /config/locales/client.ca.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | ca: 8 | js: 9 | zoom: 10 | remove: "Elimina" 11 | webinar_picker: 12 | create: "Afegeix" 13 | register: "Registre" 14 | -------------------------------------------------------------------------------- /config/locales/client.cs.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | cs: 8 | js: 9 | zoom: 10 | remove: "Smazat" 11 | webinar_picker: 12 | create: "přidat" 13 | register: "Registrovat" 14 | hours: "hodin" 15 | -------------------------------------------------------------------------------- /config/locales/client.da.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | da: 8 | js: 9 | zoom: 10 | remove: "Fjern" 11 | webinar_picker: 12 | create: "Tilføj" 13 | register: "Registrer" 14 | hours: "Timer" 15 | -------------------------------------------------------------------------------- /config/locales/client.de.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | de: 8 | admin_js: 9 | admin: 10 | site_settings: 11 | categories: 12 | discourse_zoom: "Discourse – Zoom" 13 | site_settings: 14 | zoom_enabled: "Zoom-Plug-in aktiviert" 15 | zoom_send_reminder_minutes_before_webinar: "Wie viele Minuten vor Beginn eines Webinars sollen Erinnerungsbenachrichtigungen an die Teilnehmer gesendet werden? Wert leer lassen oder auf 0 setzen, wenn keine Benachrichtigungen verschickt werden sollen." 16 | js: 17 | zoom: 18 | remove: "Entfernen" 19 | remove_webinar: "Webinar entfernen" 20 | edit_webinar: "Webinar bearbeiten" 21 | add_webinar: "Webinar hinzufügen" 22 | confirm_remove: "Bist du sicher, dass du dieses Webinar entfernen möchtest?" 23 | toggle_edit_webinar_button: "Webinar bearbeiten" 24 | webinar_picker: 25 | webinar_id: "Webinar-ID" 26 | title: "Webinar hinzufügen" 27 | create: "Hinzufügen" 28 | button: "Zoom-Webinar" 29 | clear: "Wähle ein anderes Webinar aus" 30 | register: "Registrieren" 31 | registered: "Registriert" 32 | panelists: "Diskussionsteilnehmer" 33 | attendees: "Teilnehmer" 34 | no_user_webinars: "Du bist für keine Webinare registriert." 35 | webinars_title: "Webinare" 36 | select_panelist: "Benutzername oder E-Mail-Adresse" 37 | add_panelist: "Diskussionsteilnehmer hinzufügen" 38 | nonzoom_details: "Metadaten des vergangenen Webinars" 39 | host: "Gastgeber" 40 | select_host: "Benutzername oder E-Mail-Adresse" 41 | title_date: "Titel und Datum" 42 | hosted_by: "Veranstaltet von" 43 | countdown_label: "Event beginnt in" 44 | days: "Tage" 45 | hours: "Stunden" 46 | mins: "Min." 47 | secs: "Sek." 48 | no_panelists: "Diesem Webinar sind keine Diskussionsteilnehmer zugeordnet. Verwende das Formular unten, um einen Diskussionsteilnehmer hinzuzufügen." 49 | no_panelists_preview: "Diesem Webinar sind keine Diskussionsteilnehmer zugeordnet. Du kannst sie später hinzufügen." 50 | error: "Es ist ein Fehler aufgetreten, bitte versuche es noch einmal. " 51 | join_sdk: "Jetzt beitreten!" 52 | cancel_registration: "Registrierung stornieren" 53 | no_registration_required: 'Dieses Webinar ist in Zoom mit „Registrierung erforderlich“ markiert, was derzeit nicht unterstützt wird. Bitte bearbeite die Webinareinstellungen und versuche es erneut.' 54 | webinar_existing_topic: "Dieses Webinar ist bereits mit einem Thema verknüpft. Themen-ID: %{topic_id}" 55 | webinar_ended: "Dieses Event ist nun vorbei." 56 | webinar_recording: "Webinar-Aufzeichnung hinzufügen" 57 | webinar_recording_description: "Gib die URL der Videoaufzeichnung für dieses Webinar ein. " 58 | show_recording: "Erneut abspielen" 59 | add_to_google_calendar: "Zum Google-Kalender hinzufügen" 60 | add_to_outlook: "Zu Outlook hinzufügen" 61 | add_to_calendar: "Zum Kalender hinzufügen" 62 | webinar_footnote: "Du kannst nicht am Event teilnehmen? Registriere dich und wir schicken dir nach dem Event eine Zusammenfassung zu." 63 | webinar_logged_in_users_only: "Die Webinar-Registrierung ist nur für angemeldete Benutzer zugänglich." 64 | add_past_webinar: "Vergangenes Webinar hinzufügen" 65 | past_label: "Titel des vergangenen Webinars" 66 | past_date: "Datum des vergangenen Webinars" 67 | -------------------------------------------------------------------------------- /config/locales/client.el.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | el: 8 | js: 9 | zoom: 10 | remove: "Αφαίρεση" 11 | webinar_picker: 12 | create: "Προσθήκη" 13 | register: "Εγγραφή" 14 | hours: "Ώρες" 15 | -------------------------------------------------------------------------------- /config/locales/client.en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | admin_js: 3 | admin: 4 | site_settings: 5 | categories: 6 | discourse_zoom: "Discourse Zoom" 7 | site_settings: 8 | zoom_enabled: "Zoom plugin enabled" 9 | zoom_send_reminder_minutes_before_webinar: "How many minutes before a webinar starts, do you want reminder notifications sent to attendees? Set to empty or 0 if you do not want notifications sent." 10 | 11 | js: 12 | zoom: 13 | remove: "Remove" 14 | remove_webinar: "Remove Webinar" 15 | edit_webinar: "Edit Webinar" 16 | add_webinar: "Add Webinar" 17 | confirm_remove: "Are you sure you want to remove this webinar?" 18 | 19 | toggle_edit_webinar_button: "Edit Webinar" 20 | webinar_picker: 21 | webinar_id: "Webinar ID" 22 | title: "Add Webinar" 23 | create: "Add" 24 | button: "Zoom Webinar" 25 | clear: "Choose a different webinar" 26 | register: "Register" 27 | registered: "Registered" 28 | panelists: "Panelists" 29 | attendees: "Attendees" 30 | no_user_webinars: "You are not registered to any webinars." 31 | webinars_title: "Webinars" 32 | select_panelist: "Username or email" 33 | add_panelist: "Add panelist" 34 | nonzoom_details: "Past Webinar Metadata" 35 | host: "Host" 36 | select_host: "Username or email" 37 | title_date: "Title and Date" 38 | hosted_by: "Hosted by" 39 | countdown_label: "Event begins in" 40 | days: "Days" 41 | hours: "Hours" 42 | mins: "Mins" 43 | secs: "Secs" 44 | no_panelists: "This webinar has no associated panelists. Use the form below to add a panelist." 45 | no_panelists_preview: "This webinar has no associated panelists. You can add them later." 46 | error: "There was an error, please try again. " 47 | join_sdk: "Join Now!" 48 | cancel_registration: "Cancel Registration" 49 | no_registration_required: 'This webinar is marked as "Registration required" in Zoom which is currently not supported. Please edit the webinar settings and try again.' 50 | webinar_existing_topic: "This webinar is already associated with a topic. Topic ID: %{topic_id}" 51 | webinar_ended: "This event has now ended." 52 | webinar_recording: "Add Webinar Recording" 53 | webinar_recording_description: "Enter the URL of the video recording for this webinar. " 54 | show_recording: "Play again" 55 | add_to_google_calendar: "Add to Google Calendar" 56 | add_to_outlook: "Add to Outlook" 57 | add_to_calendar: "Add to Calendar" 58 | webinar_footnote: "Can't make the event? Register and we'll send you a post-event recap." 59 | webinar_logged_in_users_only: "Webinar registration is only accessible to logged-in users." 60 | add_past_webinar: "Add past webinar" 61 | past_label: "Past webinar title" 62 | past_date: "Past webinar date" 63 | -------------------------------------------------------------------------------- /config/locales/client.en_GB.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | en_GB: 8 | -------------------------------------------------------------------------------- /config/locales/client.es.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | es: 8 | admin_js: 9 | admin: 10 | site_settings: 11 | categories: 12 | discourse_zoom: "Zoom de Discourse" 13 | site_settings: 14 | zoom_enabled: "Plugin de Zoom habilitado" 15 | zoom_send_reminder_minutes_before_webinar: "¿Cuántos minutos antes de que empiece un seminario web quieres que se envíen notificaciones recordatorias a los asistentes? Establécelo como vacío o 0 si no quieres que se envíen notificaciones." 16 | js: 17 | zoom: 18 | remove: "Eliminar" 19 | remove_webinar: "Eliminar seminario web" 20 | edit_webinar: "Editar seminario web" 21 | add_webinar: "Añadir seminario web" 22 | confirm_remove: "¿Seguro que quieres eliminar este seminario web?" 23 | toggle_edit_webinar_button: "Editar seminario web" 24 | webinar_picker: 25 | webinar_id: "ID del seminario web" 26 | title: "Añadir seminario web" 27 | create: "Añadir" 28 | button: "Seminario web en Zoom" 29 | clear: "Elige un seminario web diferente" 30 | register: "Registrarse" 31 | registered: "Registrado" 32 | panelists: "Ponentes" 33 | attendees: "Asistentes" 34 | no_user_webinars: "No estás inscrito en ningún seminario web." 35 | webinars_title: "Seminarios web" 36 | select_panelist: "Nombre de usuario o correo electrónico" 37 | add_panelist: "Añadir ponente" 38 | nonzoom_details: "Metadatos de seminarios web anteriores" 39 | host: "Anfitrión" 40 | select_host: "Nombre de usuario o correo electrónico" 41 | title_date: "Título y fecha" 42 | hosted_by: "Organizado por" 43 | countdown_label: "El evento comienza en" 44 | days: "Días" 45 | hours: "Horas" 46 | mins: "Minutos" 47 | secs: "Segundos" 48 | no_panelists: "Este seminario web no tiene ponentes asociados. Utiliza el siguiente formulario para añadir un ponente." 49 | no_panelists_preview: "Este seminario web no tiene ponentes asociados. Puedes añadirlos más adelante." 50 | error: "Se ha producido un error, inténtalo de nuevo. " 51 | join_sdk: "¡Únete ahora!" 52 | cancel_registration: "Cancelar registro" 53 | no_registration_required: 'Este seminario web está marcado como «Requiere estar inscrito» en Zoom, lo que actualmente no es compatible. Edita la configuración del seminario web e inténtalo de nuevo.' 54 | webinar_existing_topic: "Este seminario web ya está asociado a un tema. ID del tema: %{topic_id}" 55 | webinar_ended: "Este evento ya ha terminado." 56 | webinar_recording: "Añadir grabación de seminario web" 57 | webinar_recording_description: "Introduce la URL de la grabación de vídeo de este seminario web. " 58 | show_recording: "Reproducir de nuevo" 59 | add_to_google_calendar: "Añadir a calendario de Google" 60 | add_to_outlook: "Añadir a Outlook" 61 | add_to_calendar: "Añadir a Calendar" 62 | webinar_footnote: "¿No puedes asistir al evento? Inscríbete y te enviaremos un resumen posterior." 63 | webinar_logged_in_users_only: "La inscripción al seminario solo es accesible para los usuarios conectados." 64 | add_past_webinar: "Añadir seminario web anterior" 65 | past_label: "Título del seminario web anterior" 66 | past_date: "Fecha del seminario web anterior" 67 | -------------------------------------------------------------------------------- /config/locales/client.et.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | et: 8 | js: 9 | zoom: 10 | remove: "Eemalda" 11 | webinar_picker: 12 | create: "Lisa" 13 | register: "Registreeru" 14 | hours: "tundi" 15 | -------------------------------------------------------------------------------- /config/locales/client.fa_IR.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | fa_IR: 8 | site_settings: 9 | zoom_enabled: "افزونه Zoom فعال شد" 10 | js: 11 | zoom: 12 | remove: "حذف" 13 | remove_webinar: "حذف وبینار" 14 | edit_webinar: "ویرایش وبینار" 15 | add_webinar: "افزودن وبینار" 16 | toggle_edit_webinar_button: "ویرایش وبینار" 17 | webinar_picker: 18 | webinar_id: "شناسه وبینار" 19 | title: "افزودن وبینار" 20 | create: "افزودن" 21 | register: "ثبت نام" 22 | attendees: "شرکت کنندگان" 23 | webinars_title: "وبینارها" 24 | select_panelist: "نام‌کاربری یا ایمیل" 25 | host: "میزبان" 26 | select_host: "نام‌کاربری یا ایمیل" 27 | title_date: "عنوان و تاریخ" 28 | hosted_by: "میزبانی شده توسط" 29 | days: "روز" 30 | hours: "ساعت" 31 | mins: "دقیقه" 32 | secs: "ثانیه" 33 | show_recording: "پخش دوباره" 34 | add_to_google_calendar: "افزودن به تقویم گوگل" 35 | add_to_outlook: "افزودن به Outlook" 36 | add_to_calendar: "افزودن به تقویم" 37 | -------------------------------------------------------------------------------- /config/locales/client.fi.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | fi: 8 | admin_js: 9 | admin: 10 | site_settings: 11 | categories: 12 | discourse_zoom: "Discourse Zoom" 13 | site_settings: 14 | zoom_enabled: "Zoom-lisäosa käytössä" 15 | zoom_send_reminder_minutes_before_webinar: "Kuinka monta minuuttia ennen webinaarin alkua haluat, että osallistujille lähetetään muistutusilmoituksia? Jätä tyhjäksi tai aseta arvoksi 0, jos et halua, että ilmoituksia lähetetään." 16 | js: 17 | zoom: 18 | remove: "Poista" 19 | remove_webinar: "Poista webinaari" 20 | edit_webinar: "Muokkaa webinaaria" 21 | add_webinar: "Lisää webinaari" 22 | confirm_remove: "Oletko varma, että haluat poistaa tämän webinaarin?" 23 | toggle_edit_webinar_button: "Muokkaa webinaaria" 24 | webinar_picker: 25 | webinar_id: "Webinaarin tunnus" 26 | title: "Lisää webinaari" 27 | create: "Lisää" 28 | button: "Zoom-webinaari" 29 | clear: "Valitse toinen webinaari" 30 | register: "Rekisteröidy" 31 | registered: "Rekisteröity" 32 | panelists: "Panelistit" 33 | attendees: "Osallistujat" 34 | no_user_webinars: "Et ole rekisteröitynyt webinaareihin." 35 | webinars_title: "Webinaarit" 36 | select_panelist: "Käyttäjätunnus tai sähköpostiosoite" 37 | add_panelist: "Lisää panelisti" 38 | nonzoom_details: "Aiemman webinaarin metatiedot" 39 | host: "Isäntä" 40 | select_host: "Käyttäjätunnus tai sähköpostiosoite" 41 | title_date: "Otsikko ja päivämäärä" 42 | hosted_by: "Isäntä" 43 | countdown_label: "Tapahtuman alkamiseen" 44 | days: "Päivää" 45 | hours: "Tuntia" 46 | mins: "Minuuttia" 47 | secs: "Sekuntia" 48 | no_panelists: "Tässä webinaarissa ei ole panelisteja. Lisää panelisti alla olevalla lomakkeella." 49 | no_panelists_preview: "Tässä webinaarissa ei ole panelisteja. Voit lisätä heitä myöhemmin." 50 | error: "Tapahtui virhe, yritä uudelleen. " 51 | join_sdk: "Liity nyt!" 52 | cancel_registration: "Peruuta rekisteröinti" 53 | no_registration_required: 'Tämä webinaari on merkitty rekisteröitymistä edellyttäväksi Zoomissa, mitä ei tueta tällä hetkellä. Muokkaa webinaarin asetuksia ja yritä uudelleen.' 54 | webinar_existing_topic: "Tämä webinaari on jo liitetty ketjuun. Ketjun tunnus: %{topic_id}" 55 | webinar_ended: "Tämä tapahtuma on nyt päättynyt." 56 | webinar_recording: "Lisää webinaarin tallenne" 57 | webinar_recording_description: "Anna tämän webinaarin videotallenteen URL-osoite. " 58 | show_recording: "Toista uudelleen" 59 | add_to_google_calendar: "Lisää Google-kalenteriin" 60 | add_to_outlook: "Lisää Outlookiin" 61 | add_to_calendar: "Lisää kalenteriin" 62 | webinar_footnote: "Etkö pääse tapahtumaan? Rekisteröidy, niin lähetämme sinulle tapahtuman jälkeisen yhteenvedon." 63 | webinar_logged_in_users_only: "Webinaariin rekisteröityminen on vain kirjautuneiden käyttäjien käytettävissä." 64 | add_past_webinar: "Lisää aiempi webinaari" 65 | past_label: "Aiemman webinaarin otsikko" 66 | past_date: "Aiemman webinaarin päivämäärä" 67 | -------------------------------------------------------------------------------- /config/locales/client.fr.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | fr: 8 | admin_js: 9 | admin: 10 | site_settings: 11 | categories: 12 | discourse_zoom: "Discourse Zoom" 13 | site_settings: 14 | zoom_enabled: "Extension Zoom activée" 15 | zoom_send_reminder_minutes_before_webinar: "Combien de minutes avant le début d'un webinaire souhaitez-vous que des notifications de rappel soient envoyées aux participants ? Définissez cette valeur sur 0 ou laissez le champ vide si vous ne souhaitez pas que des notifications soient envoyées." 16 | js: 17 | zoom: 18 | remove: "Supprimer" 19 | remove_webinar: "Supprimer le webinaire" 20 | edit_webinar: "Modifier le webinaire" 21 | add_webinar: "Ajouter un webinaire" 22 | confirm_remove: "Voulez-vous vraiment supprimer ce webinaire ?" 23 | toggle_edit_webinar_button: "Modifier le webinaire" 24 | webinar_picker: 25 | webinar_id: "ID de webinaire" 26 | title: "Ajouter un webinaire" 27 | create: "Ajouter" 28 | button: "Webinaire Zoom" 29 | clear: "Choisissez un autre webinaire" 30 | register: "Enregistrer" 31 | registered: "Enregistré" 32 | panelists: "Panélistes" 33 | attendees: "Participants" 34 | no_user_webinars: "Vous n'êtes inscrit(e) à aucun webinaire." 35 | webinars_title: "Webinaires" 36 | select_panelist: "Nom d'utilisateur ou adresse e-mail" 37 | add_panelist: "Ajouter un panéliste" 38 | nonzoom_details: "Métadonnées des webinaires précédents" 39 | host: "Hôte" 40 | select_host: "Nom d'utilisateur ou adresse e-mail" 41 | title_date: "Titre et date" 42 | hosted_by: "Organisé par" 43 | countdown_label: "L'événement commence dans" 44 | days: "Jours" 45 | hours: "Heures" 46 | mins: "Minutes" 47 | secs: "secondes" 48 | no_panelists: "Ce webinaire n'a aucun panéliste associé. Utilisez le formulaire ci-dessous pour ajouter un intervenant." 49 | no_panelists_preview: "Ce webinaire n'a aucun panéliste associé. Vous pourrez en ajouter ultérieurement." 50 | error: "Une erreur s'est produite. Veuillez réessayer. " 51 | join_sdk: "Rejoignez maintenant !" 52 | cancel_registration: "Annuler l'inscription" 53 | no_registration_required: 'Ce webinaire est marqué comme « Inscription requise » dans Zoom, ce qui n''est actuellement pas pris en charge. Modifiez les paramètres du webinaire et réessayez.' 54 | webinar_existing_topic: "Ce webinaire est déjà associé à un sujet. ID de sujet : %{topic_id}" 55 | webinar_ended: "Cet événement est maintenant terminé." 56 | webinar_recording: "Ajouter un enregistrement de webinaire" 57 | webinar_recording_description: "Saisissez l'URL de l'enregistrement vidéo de ce webinaire. " 58 | show_recording: "Rejouer" 59 | add_to_google_calendar: "Ajouter au calendrier Google" 60 | add_to_outlook: "Ajouter à Outlook" 61 | add_to_calendar: "Ajouter au calendrier" 62 | webinar_footnote: "Vous ne pouvez pas participer à l'événement ? Inscrivez-vous et nous vous enverrons un récapitulatif après l'événement." 63 | webinar_logged_in_users_only: "L'inscription au webinaire n'est accessible qu'aux utilisateurs connectés." 64 | add_past_webinar: "Ajouter un webinaire passé" 65 | past_label: "Titre du webinaire passé" 66 | past_date: "Date du webinaire passé" 67 | -------------------------------------------------------------------------------- /config/locales/client.gl.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | gl: 8 | js: 9 | zoom: 10 | remove: "Retirar" 11 | webinar_picker: 12 | create: "Engadir" 13 | register: "Rexistrarse" 14 | hours: "horas" 15 | -------------------------------------------------------------------------------- /config/locales/client.he.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | he: 8 | admin_js: 9 | admin: 10 | site_settings: 11 | categories: 12 | discourse_zoom: "Discourse Zoom" 13 | site_settings: 14 | zoom_enabled: "תוסף זום מופעל" 15 | zoom_send_reminder_minutes_before_webinar: "כמה דקות לפני תחילת ובינר יש לשלוח התראות תזכורת למשתתפים? יש להשאיר ריק או 0 כדי שלא תישלחנה התראות." 16 | js: 17 | zoom: 18 | remove: "הסרה" 19 | remove_webinar: "הסרת ובינר" 20 | edit_webinar: "עריכת ובינר" 21 | add_webinar: "הוספת ובינר" 22 | confirm_remove: "להסיר את הוובינר הזה?" 23 | toggle_edit_webinar_button: "עריכת ובינר" 24 | webinar_picker: 25 | webinar_id: "מזהה ובינר" 26 | title: "הוספת ובינר" 27 | create: "הוספה" 28 | button: "ובינר בזום" 29 | clear: "בחירת ובינר אחר" 30 | register: "הרשמה" 31 | registered: "נרשמת" 32 | panelists: "חברי פאנל" 33 | attendees: "משתתפים" 34 | no_user_webinars: "לא נרשמת לאף ובינר." 35 | webinars_title: "ובינרים" 36 | select_panelist: "שם משתמש או דוא״ל" 37 | add_panelist: "הוספת חבר פאנל" 38 | nonzoom_details: "נתוני־על של ובינר עבר" 39 | host: "מנחה" 40 | select_host: "שם משתמש או דוא״ל" 41 | title_date: "כותרת ותאריך" 42 | hosted_by: "בחסות" 43 | countdown_label: "האירוע מתחיל ב־" 44 | days: "ימים" 45 | hours: "שעות" 46 | mins: "דקות" 47 | secs: "שניות" 48 | no_panelists: "אין משתתפי פאנל שמשויכים לוובינר הזה. יש להשתמש בטופס שלהלן כדי להוסיף אותם." 49 | no_panelists_preview: "אין משתתפי פאנל שמשויכים לוובינר הזה. אפשר להוסיף אותם אחר כך." 50 | error: "אירעה שגיאה, נא לנסות שוב. " 51 | join_sdk: "הצטרפות כעת!" 52 | cancel_registration: "ביטול הרשמה" 53 | no_registration_required: 'ובינר זה מסומן כ„דורש הרשמה” בזום ואין בזה תמיכה כרגע. נא לערוך את הגדרות הוובינר ולנסות שוב.' 54 | webinar_existing_topic: "ובינר זה כבר משויך לנושא. מזהה נושא: %{topic_id}" 55 | webinar_ended: "האירוע הזה כבר הסתיים." 56 | webinar_recording: "הוספת הקלטת ובינר" 57 | webinar_recording_description: "נא למלא את כתובת הקלטת הווידאו של הוובינר הזה. " 58 | show_recording: "לנגן שוב" 59 | add_to_google_calendar: "הוספה ללוח השנה של Google" 60 | add_to_outlook: "הוספה ל־Outlook" 61 | add_to_calendar: "הוספה ללוח השנה" 62 | webinar_footnote: "לא יתאפשר לך לקחת חלק באירוע? כדאי להירשם כדי שנשלח לך סיכום לאחר האירוע." 63 | webinar_logged_in_users_only: "הרשמה לוובינר זמינה רק למשתמשים שנכנסו למערכת." 64 | add_past_webinar: "הוספת ובינר עבר" 65 | past_label: "כותרת ובינר עבר" 66 | past_date: "תאריך ובינר קודם" 67 | -------------------------------------------------------------------------------- /config/locales/client.hr.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | hr: 8 | js: 9 | zoom: 10 | remove: "Ukloni" 11 | webinar_picker: 12 | create: "Dodaj" 13 | register: "Registracija" 14 | hours: "Sata" 15 | -------------------------------------------------------------------------------- /config/locales/client.hu.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | hu: 8 | js: 9 | zoom: 10 | remove: "Eltávolítás" 11 | webinar_picker: 12 | create: "Hozzáad" 13 | register: "Regisztráció" 14 | hours: "óráig" 15 | -------------------------------------------------------------------------------- /config/locales/client.hy.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | hy: 8 | js: 9 | zoom: 10 | remove: "Հեռացնել" 11 | webinar_picker: 12 | create: "Ավելացնել" 13 | register: "Գրանցվել" 14 | -------------------------------------------------------------------------------- /config/locales/client.id.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | id: 8 | js: 9 | zoom: 10 | remove: "Hapus" 11 | webinar_picker: 12 | create: "Menambahkan" 13 | -------------------------------------------------------------------------------- /config/locales/client.it.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | it: 8 | admin_js: 9 | admin: 10 | site_settings: 11 | categories: 12 | discourse_zoom: "Discourse Zoom" 13 | site_settings: 14 | zoom_enabled: "Plugin Zoom abilitato" 15 | zoom_send_reminder_minutes_before_webinar: "Quanti minuti prima dell'inizio di un webinar vuoi che vengano inviate notifiche di promemoria ai partecipanti? Imposta su vuoto o 0 se non si desidera inviare notifiche." 16 | js: 17 | zoom: 18 | remove: "Rimuovi" 19 | remove_webinar: "Rimuovi webinar" 20 | edit_webinar: "Modifica webinar" 21 | add_webinar: "Aggiungi webinar" 22 | confirm_remove: "Vuoi davvero rimuovere questo webinar?" 23 | toggle_edit_webinar_button: "Modifica webinar" 24 | webinar_picker: 25 | webinar_id: "ID webinar" 26 | title: "Aggiungi webinar" 27 | create: "Aggiungi" 28 | button: "Webinar Zoom" 29 | clear: "Scegli un altro webinar" 30 | register: "Registrati" 31 | registered: "Registrazione effettuata" 32 | panelists: "Relatori" 33 | attendees: "Partecipanti" 34 | no_user_webinars: "Non sei registrato a nessun webinar." 35 | webinars_title: "Webinar" 36 | select_panelist: "Nome utente o e-mail" 37 | add_panelist: "Aggiungi relatore" 38 | nonzoom_details: "Metadati dei webinar precedenti" 39 | host: "Ospite" 40 | select_host: "Nome utente o e-mail" 41 | title_date: "Titolo e data" 42 | hosted_by: "Ospitato da" 43 | countdown_label: "L'evento inizia tra" 44 | days: "Giorni" 45 | hours: "Ore" 46 | mins: "Minuti" 47 | secs: "Secondi" 48 | no_panelists: "Questo webinar non ha relatori associati. Utilizza il modulo sottostante per aggiungere un relatore." 49 | no_panelists_preview: "Questo webinar non ha relatori associati. Puoi aggiungerli in seguito." 50 | error: "Si è verificato un errore, riprova. " 51 | join_sdk: "Iscriviti subito!" 52 | cancel_registration: "Annulla registrazione" 53 | no_registration_required: 'Questo webinar è contrassegnato come "Registrazione richiesta" in Zoom, che al momento non è supportato. Modifica le impostazioni del webinar e riprova.' 54 | webinar_existing_topic: "Questo webinar è già associato a un argomento. ID argomento: %{topic_id}" 55 | webinar_ended: "Questo evento è terminato." 56 | webinar_recording: "Aggiungi la registrazione del webinar" 57 | webinar_recording_description: "Inserisci l'URL della registrazione video per questo webinar. " 58 | show_recording: "Riproduci di nuovo" 59 | add_to_google_calendar: "Aggiungi a Google Calendar" 60 | add_to_outlook: "Aggiungi a Outlook" 61 | add_to_calendar: "Aggiungi al calendario" 62 | webinar_footnote: "Non riesci a presentarti all'evento? Registrati e ti invieremo un riepilogo post-evento." 63 | webinar_logged_in_users_only: "La registrazione al webinar è accessibile solo agli utenti registrati." 64 | add_past_webinar: "Aggiungi webinar precedente" 65 | past_label: "Titolo del webinar precedente" 66 | past_date: "Data del webinar precedente" 67 | -------------------------------------------------------------------------------- /config/locales/client.ja.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | ja: 8 | admin_js: 9 | admin: 10 | site_settings: 11 | categories: 12 | discourse_zoom: "Discourse Zoom" 13 | site_settings: 14 | zoom_enabled: "Zoom プラグインが有効です" 15 | zoom_send_reminder_minutes_before_webinar: "ウェビナーが開始する何分前に、出席者にリマインダー通知を送信しますか?通知を送信しない場合は、空にするか 0 を設定します。" 16 | js: 17 | zoom: 18 | remove: "削除" 19 | remove_webinar: "ウェビナーを削除" 20 | edit_webinar: "ウェビナーを編集" 21 | add_webinar: "ウェビナーを追加" 22 | confirm_remove: "このウェビナーを削除してもよろしいですか?" 23 | toggle_edit_webinar_button: "ウェビナーを編集" 24 | webinar_picker: 25 | webinar_id: "ウェビナー ID" 26 | title: "ウェビナーを追加" 27 | create: "追加" 28 | button: "Zoom ウェビナー" 29 | clear: "別のウェビナーを選択する" 30 | register: "登録" 31 | registered: "登録済み" 32 | panelists: "パネリスト" 33 | attendees: "出席者" 34 | no_user_webinars: "どのウェビナーにも登録されていません。" 35 | webinars_title: "ウェビナー" 36 | select_panelist: "ユーザー名またはメール" 37 | add_panelist: "パネリストの追加" 38 | nonzoom_details: "過去のウェビナーのメタデータ" 39 | host: "ホスト" 40 | select_host: "ユーザー名またはメール" 41 | title_date: "タイトルと日付" 42 | hosted_by: "主催者:" 43 | countdown_label: "イベント開始まで後" 44 | days: "日" 45 | hours: "時間" 46 | mins: "分" 47 | secs: "秒" 48 | no_panelists: "このウェビナーに関連するパネリストはいません。以下のフォームを使ってパネリストを追加してください。" 49 | no_panelists_preview: "このウェビナーには関連するパネリストがいません。後で追加することができます。" 50 | error: "エラーが発生しました。もう一度お試しください。 " 51 | join_sdk: "今すぐ参加!" 52 | cancel_registration: "登録をキャンセル" 53 | no_registration_required: 'このウェビナーは、Zoom で「登録が必要」としてマークされていますが、現在サポートされていません。ウェビナーの設定を編集してからもう一度お試しください。' 54 | webinar_existing_topic: "このウェビナーはすでにトピックに関連付けられています。トピック ID: %{topic_id}" 55 | webinar_ended: "このイベントは終了しました。" 56 | webinar_recording: "ウェビナーの録画を追加" 57 | webinar_recording_description: "このウェビナーのビデオ録画の URL を入力します。 " 58 | show_recording: "もう一度再生" 59 | add_to_google_calendar: "Google カレンダーに追加" 60 | add_to_outlook: "Outlook に追加" 61 | add_to_calendar: "カレンダーに追加" 62 | webinar_footnote: "イベントに参加できませんか?登録すると、イベント後のまとめをお送りします。" 63 | webinar_logged_in_users_only: "ウェビナー登録は、ログインしたユーザーのみがアクセスできます。" 64 | add_past_webinar: "過去のウェビナーを追加する" 65 | past_label: "過去のウェビナーのタイトル" 66 | past_date: "過去のウェビナーの日付" 67 | -------------------------------------------------------------------------------- /config/locales/client.ko.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | ko: 8 | js: 9 | zoom: 10 | remove: "제거" 11 | webinar_picker: 12 | create: "추가" 13 | register: "등록하기" 14 | hours: "시간" 15 | -------------------------------------------------------------------------------- /config/locales/client.lt.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | lt: 8 | js: 9 | zoom: 10 | remove: "Pašalinti" 11 | webinar_picker: 12 | create: "Pridėti" 13 | register: "Registruokis" 14 | hours: "valandos" 15 | -------------------------------------------------------------------------------- /config/locales/client.lv.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | lv: 8 | js: 9 | zoom: 10 | remove: "Atcelt" 11 | webinar_picker: 12 | create: "Pievienot" 13 | register: "Reģistrēties" 14 | -------------------------------------------------------------------------------- /config/locales/client.nb_NO.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | nb_NO: 8 | js: 9 | zoom: 10 | remove: "Fjern" 11 | webinar_picker: 12 | create: "Legg til" 13 | register: "Registrer" 14 | hours: "timer" 15 | -------------------------------------------------------------------------------- /config/locales/client.nl.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | nl: 8 | admin_js: 9 | admin: 10 | site_settings: 11 | categories: 12 | discourse_zoom: "Discourse Zoom" 13 | site_settings: 14 | zoom_enabled: "Zoom-plug-in ingeschakeld" 15 | zoom_send_reminder_minutes_before_webinar: "Hoeveel minuten voordat een webinar begint wil je dat er herinneringsmeldingen naar deelnemers worden gestuurd? Stel dit in op leeg of 0 als je geen meldingen wilt sturen." 16 | js: 17 | zoom: 18 | remove: "Verwijderen" 19 | remove_webinar: "Webinar verwijderen" 20 | edit_webinar: "Webinar bewerken" 21 | add_webinar: "Webinar toevoegen" 22 | confirm_remove: "Weet je zeker dat je dit webinar wilt verwijderen?" 23 | toggle_edit_webinar_button: "Webinar bewerken" 24 | webinar_picker: 25 | webinar_id: "Webinar-ID" 26 | title: "Webinar toevoegen" 27 | create: "Toevoegen" 28 | button: "Zoom-webinar" 29 | clear: "Kies een ander webinar" 30 | register: "Registreren" 31 | registered: "Geregistreerd" 32 | panelists: "Panelleden" 33 | attendees: "Deelnemers" 34 | no_user_webinars: "Je bent niet geregistreerd voor webinars." 35 | webinars_title: "Webinars" 36 | select_panelist: "Gebruikersnaam of e-mailadres" 37 | add_panelist: "Panellid toevoegen" 38 | nonzoom_details: "Metagegevens eerdere webinars" 39 | host: "Host" 40 | select_host: "Gebruikersnaam of e-mailadres" 41 | title_date: "Titel en datum" 42 | hosted_by: "Gehost door" 43 | countdown_label: "Evenement begint over" 44 | days: "Dagen" 45 | hours: "Uur" 46 | mins: "Min." 47 | secs: "Sec." 48 | no_panelists: "Dit webinar heeft geen bijbehorende panelleden. Gebruik het formulier hieronder om een panellid toe te voegen." 49 | no_panelists_preview: "Deze webinar heeft geen bijbehorende panelleden. Je kunt ze later toevoegen." 50 | error: "Er was een fout, probeer het opnieuw. " 51 | join_sdk: "Nu deelnemen!" 52 | cancel_registration: "Registratie annuleren" 53 | no_registration_required: 'Dit webinar is gemarkeerd als "Registratie vereist" in Zoom, wat momenteel niet wordt ondersteund. Pas de webinarinstellingen aan en probeer het opnieuw.' 54 | webinar_existing_topic: "Dit webinar is al gekoppeld aan een topic. Topic-ID: %{topic_id}" 55 | webinar_ended: "Dit evenement is afgelopen." 56 | webinar_recording: "Webinaropname toevoegen" 57 | webinar_recording_description: "Voer de URL in van de video-opname voor dit webinar. " 58 | show_recording: "Opnieuw afspelen" 59 | add_to_google_calendar: "Toevoegen aan Google Agenda" 60 | add_to_outlook: "Toevoegen aan Outlook" 61 | add_to_calendar: "Toevoegen aan Agenda" 62 | webinar_footnote: "Kun je het evenement niet bijwonen? Registreer en we sturen je een samenvatting na het evenement." 63 | webinar_logged_in_users_only: "Webinarregistratie is alleen toegankelijk voor ingelogde gebruikers." 64 | add_past_webinar: "Eerder webinar toevoegen" 65 | past_label: "Titel eerder webinar" 66 | past_date: "Datum eerder webinar" 67 | -------------------------------------------------------------------------------- /config/locales/client.pl_PL.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | pl_PL: 8 | site_settings: 9 | zoom_enabled: "Wtyczka Zoom włączona" 10 | zoom_send_reminder_minutes_before_webinar: "Ile minut przed rozpoczęciem webinaru chcesz, aby uczestnicy otrzymywali powiadomienia przypominające? Ustaw pustą wartość lub 0, jeśli nie chcesz, aby powiadomienia były wysyłane." 11 | js: 12 | zoom: 13 | remove: "Usuń" 14 | remove_webinar: "Usuń webinar" 15 | edit_webinar: "Edytuj webinar" 16 | add_webinar: "Dodaj webinar" 17 | confirm_remove: "Czy na pewno chcesz usunąć ten webinar?" 18 | toggle_edit_webinar_button: "Edytuj webinar" 19 | webinar_picker: 20 | webinar_id: "Identyfikator webinaru" 21 | title: "Dodaj webinar" 22 | create: "Dodaj" 23 | button: "Webinar Zoom" 24 | clear: "Wybierz inny webinar" 25 | register: "Zarejestruj" 26 | registered: "Zarejestrowany" 27 | panelists: "Członkowie panelu" 28 | attendees: "Uczestnicy" 29 | no_user_webinars: "Nie jesteś zarejestrowany na żadne webinary." 30 | webinars_title: "Webinary" 31 | select_panelist: "Nazwa użytkownika lub email" 32 | add_panelist: "Dodaj członków panelu" 33 | nonzoom_details: "Metadane poprzedniego webinaru" 34 | host: "Prowadzący" 35 | select_host: "Nazwa użytkownika lub email" 36 | title_date: "Tytuł i data" 37 | hosted_by: "Prowadzony przez" 38 | countdown_label: "Wydarzenie rozpoczyna się za" 39 | days: "Dni" 40 | hours: "Godzin" 41 | mins: "Min" 42 | secs: "Sek" 43 | no_panelists: "Ten webinar nie ma powiązanych członków panelu. Skorzystaj z poniższego formularza, aby dodać członków panelu." 44 | no_panelists_preview: "Ten webinar nie ma powiązanych członków panelu. Możesz dodać ich później." 45 | error: "Wystąpił błąd, spróbuj ponownie. " 46 | join_sdk: "Dołącz teraz!" 47 | cancel_registration: "Anuluj rejestrację" 48 | no_registration_required: 'Ten webinar jest oznaczony jako „Wymagana rejestracja” w Zoom, która obecnie nie jest obsługiwana. Zmień ustawienia webinaru i spróbuj ponownie.' 49 | webinar_existing_topic: "Ten webinar jest już powiązany z tematem. Identyfikator tematu: %{topic_id}" 50 | webinar_ended: "To wydarzenie dobiegło końca." 51 | webinar_recording: "Dodaj nagranie webinaru" 52 | webinar_recording_description: "Wprowadź adres URL nagrania wideo tego webinaru. " 53 | show_recording: "Odtwórz ponownie" 54 | add_to_google_calendar: "Dodaj do Kalendarza Google" 55 | add_to_outlook: "Dodaj do Outlooka" 56 | add_to_calendar: "Dodaj do kalendarza" 57 | webinar_footnote: "Nie możesz wziąć udziału w wydarzeniu? Zarejestruj się, a wyślemy Ci podsumowanie po wydarzeniu." 58 | webinar_logged_in_users_only: "Rejestracja na webinar jest dostępna tylko dla zalogowanych użytkowników." 59 | add_past_webinar: "Dodaj poprzedni webinar" 60 | past_label: "Poprzedni tytuł webinaru" 61 | past_date: "Data poprzedniego webinaru" 62 | -------------------------------------------------------------------------------- /config/locales/client.pt.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | pt: 8 | js: 9 | zoom: 10 | remove: "Remover" 11 | webinar_picker: 12 | create: "Adicionar" 13 | register: "Registar" 14 | hours: "Horas" 15 | -------------------------------------------------------------------------------- /config/locales/client.pt_BR.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | pt_BR: 8 | admin_js: 9 | admin: 10 | site_settings: 11 | categories: 12 | discourse_zoom: "Discourse Zoom" 13 | site_settings: 14 | zoom_enabled: "Plugin do Zoom ativado" 15 | zoom_send_reminder_minutes_before_webinar: "Com quantos minutos de antecedência até o início do webinar, você deseja enviar notificações com lembretes aos(às) participantes? Defina como vazio ou 0 se não quiser enviar notificações." 16 | js: 17 | zoom: 18 | remove: "Remover" 19 | remove_webinar: "Remover webinar" 20 | edit_webinar: "Editar webinar" 21 | add_webinar: "Adicionar webinar" 22 | confirm_remove: "Tem certeza de que deseja remover este webinar?" 23 | toggle_edit_webinar_button: "Editar webinar" 24 | webinar_picker: 25 | webinar_id: "ID do webinar" 26 | title: "Adicionar webinar" 27 | create: "Adicionar" 28 | button: "Webinar no Zoom" 29 | clear: "Escolhar um webinar diferente" 30 | register: "Registrar-se" 31 | registered: "Registro realizado" 32 | panelists: "Palestrantes" 33 | attendees: "Participantes" 34 | no_user_webinars: "Você não se registrou em nenhum webinar." 35 | webinars_title: "Webinars" 36 | select_panelist: "Nome do(a) usuário(a) ou e-mail" 37 | add_panelist: "Adicionar palestrante" 38 | nonzoom_details: "Metadados de webinar passado" 39 | host: "Hospedar" 40 | select_host: "Nome do(a) usuário(a) ou e-mail" 41 | title_date: "Título e data" 42 | hosted_by: "Organização de" 43 | countdown_label: "O evento começa em" 44 | days: "Dias" 45 | hours: "Horas" 46 | mins: "Min" 47 | secs: "s" 48 | no_panelists: "Este webinar não tem palestrantes associados(as). Use o formulário abaixo para adicionar um(a) palestrante." 49 | no_panelists_preview: "Este webinar não tem palestrantes associados(as). Você pode adicioná-los(as) mais tarde." 50 | error: "Ocorreu um erro, tente novamente. " 51 | join_sdk: "Participe agora mesmo!" 52 | cancel_registration: "Cancelar registro" 53 | no_registration_required: 'Este webinar está marcado como "Requer registro" no Zoom, que não é compatível por enquanto. Edite as configurações do webinar e tente novamente.' 54 | webinar_existing_topic: "Este webinar já está associado a um tópico. ID do tópico: %{topic_id}" 55 | webinar_ended: "Este evento já terminou." 56 | webinar_recording: "Adicionar gravação do webinar" 57 | webinar_recording_description: "Insira a URL da gravação do vídeo deste webinar. " 58 | show_recording: "Reproduzir novamente" 59 | add_to_google_calendar: "Adicionar ao Google Calendar" 60 | add_to_outlook: "Adicionar ao Outlook" 61 | add_to_calendar: "Adicionar ao calendário" 62 | webinar_footnote: "Não consegue fazer o evento? Registre-se agora e enviaremos um resumo do pós-evento." 63 | webinar_logged_in_users_only: "Apenas usuários(as) logados(as) podem acessar o registro no webinar." 64 | add_past_webinar: "Adicionar webinar anterior" 65 | past_label: "Título do webinar anterior" 66 | past_date: "Data do webinar anterior" 67 | -------------------------------------------------------------------------------- /config/locales/client.ro.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | ro: 8 | js: 9 | zoom: 10 | remove: "Elimină" 11 | webinar_picker: 12 | create: "Adaugă" 13 | register: "Înregistrare" 14 | hours: "Ore" 15 | -------------------------------------------------------------------------------- /config/locales/client.ru.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | ru: 8 | admin_js: 9 | admin: 10 | site_settings: 11 | categories: 12 | discourse_zoom: "Плагин Zoom для Discourse" 13 | site_settings: 14 | zoom_enabled: "Плагин Zoom включен" 15 | zoom_send_reminder_minutes_before_webinar: "За сколько минут до начала вебинара отправлять уведомления с напоминанием участникам? Пустое значение или 0 — не отправлять уведомления." 16 | js: 17 | zoom: 18 | remove: "Удалить" 19 | remove_webinar: "Удалить вебинар" 20 | edit_webinar: "Изменить вебинар" 21 | add_webinar: "Добавить вебинар" 22 | confirm_remove: "Действительно удалить вебинар?" 23 | toggle_edit_webinar_button: "Изменить вебинар" 24 | webinar_picker: 25 | webinar_id: "Идентификатор вебинара" 26 | title: "Добавление вебинара" 27 | create: "Добавить" 28 | button: "Zoom-вебинар" 29 | clear: "Выбрать другой вебинар" 30 | register: "Зарегистрироваться" 31 | registered: "Регистрация выполнена" 32 | panelists: "Участники дискуссии" 33 | attendees: "Зрители" 34 | no_user_webinars: "Вы не зарегистрированы ни на один вебинар." 35 | webinars_title: "Вебинары" 36 | select_panelist: "Имя пользователя или эл. почта" 37 | add_panelist: "Добавить участника дискуссии" 38 | nonzoom_details: "Метаданные прошедших вебинаров" 39 | host: "Организатор" 40 | select_host: "Имя пользователя или эл. почта" 41 | title_date: "Название и дата" 42 | hosted_by: "Организатор" 43 | countdown_label: "До начала события" 44 | days: "Дни" 45 | hours: "Часы" 46 | mins: "Минуты" 47 | secs: "Секунды" 48 | no_panelists: "С этим вебинаром не связаны участники дискуссии — добавьте их с помощью формы ниже." 49 | no_panelists_preview: "С этим вебинаром не связаны участники дискуссии — их можно добавить позже." 50 | error: "Произошла ошибка, повторите попытку. " 51 | join_sdk: "Присоединиться!" 52 | cancel_registration: "Отменить регистрацию" 53 | no_registration_required: 'В Zoom указано, что на вебинар требуется регистрация, но эта функция сейчас не поддерживается. Измените настройки вебинара и повторите попытку.' 54 | webinar_existing_topic: "Этот вебинар уже связан с темой. Идентификатор темы: %{topic_id}" 55 | webinar_ended: "Событие завершено." 56 | webinar_recording: "Добавить запись вебинара" 57 | webinar_recording_description: "Введите URL-адрес видеозаписи для вебинара. " 58 | show_recording: "Запустить еще раз" 59 | add_to_google_calendar: "Добавить в Google Календарь" 60 | add_to_outlook: "Добавить в Outlook" 61 | add_to_calendar: "Добавить в календарь" 62 | webinar_footnote: "Не можете посетить событие? Зарегистрируйтесь, и мы пришлем вам сводку по итогам." 63 | webinar_logged_in_users_only: "Зарегистрироваться на вебинар могут только авторизованные пользователи." 64 | add_past_webinar: "Добавить прошедший вебинар" 65 | past_label: "Название прошедшего вебинара" 66 | past_date: "Дата прошедшего вебинара" 67 | -------------------------------------------------------------------------------- /config/locales/client.sk.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | sk: 8 | js: 9 | zoom: 10 | remove: "Odstrániť" 11 | webinar_picker: 12 | create: "Pridať" 13 | register: "Registrácia" 14 | hours: "Hodiny" 15 | -------------------------------------------------------------------------------- /config/locales/client.sl.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | sl: 8 | js: 9 | zoom: 10 | remove: "Odstrani" 11 | webinar_picker: 12 | create: "Dodaj" 13 | register: "Registracija" 14 | hours: "Uri" 15 | -------------------------------------------------------------------------------- /config/locales/client.sq.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | sq: 8 | js: 9 | zoom: 10 | remove: "Hiq" 11 | webinar_picker: 12 | create: "Shto" 13 | -------------------------------------------------------------------------------- /config/locales/client.sr.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | sr: 8 | js: 9 | zoom: 10 | remove: "Ukloni" 11 | webinar_picker: 12 | create: "Dodaj" 13 | -------------------------------------------------------------------------------- /config/locales/client.sv.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | sv: 8 | js: 9 | zoom: 10 | remove: "Ta bort" 11 | webinar_picker: 12 | create: "Lägg till" 13 | register: "Registrera" 14 | hours: "Timmar" 15 | -------------------------------------------------------------------------------- /config/locales/client.sw.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | sw: 8 | js: 9 | zoom: 10 | remove: "Ondoa" 11 | webinar_picker: 12 | create: "ongeza" 13 | register: "Jisajili" 14 | -------------------------------------------------------------------------------- /config/locales/client.te.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | te: 8 | js: 9 | zoom: 10 | remove: "తీసివేయండి" 11 | webinar_picker: 12 | create: "జోడించండి" 13 | register: "నమోదు చేసుకోండి" 14 | -------------------------------------------------------------------------------- /config/locales/client.th.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | th: 8 | js: 9 | zoom: 10 | remove: "ลบ" 11 | webinar_picker: 12 | create: "เพิ่ม" 13 | register: "ลงทะเบียน" 14 | -------------------------------------------------------------------------------- /config/locales/client.tr_TR.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | tr_TR: 8 | admin_js: 9 | admin: 10 | site_settings: 11 | categories: 12 | discourse_zoom: "Discourse Zoom" 13 | site_settings: 14 | zoom_enabled: "Zoom eklentisi etkin" 15 | zoom_send_reminder_minutes_before_webinar: "Bir web semineri başlamadan kaç dakika önce katılımcılara hatırlatma bildirimlerinin gönderilmesini ister misiniz? Bildirimlerin gönderilmesini istemiyorsanız boş veya 0 olarak ayarlayın." 16 | js: 17 | zoom: 18 | remove: "Kaldır" 19 | remove_webinar: "Web Seminerini Kaldır" 20 | edit_webinar: "Web Seminerini Düzenle" 21 | add_webinar: "Web Semineri Ekle" 22 | confirm_remove: "Bu web seminerini kaldırmak istediğinizden emin misiniz?" 23 | toggle_edit_webinar_button: "Web Seminerini Düzenle" 24 | webinar_picker: 25 | webinar_id: "Web semineri kimliği" 26 | title: "Web Semineri Ekle" 27 | create: "Ekle" 28 | button: "Zoom Web Semineri" 29 | clear: "Farklı bir web semineri seçin" 30 | register: "Kayıt Ol" 31 | registered: "Kayıt Olundu" 32 | panelists: "Panelistler" 33 | attendees: "Katılımcılar" 34 | no_user_webinars: "Herhangi bir web seminerine kayıtlı değilsiniz." 35 | webinars_title: "Web Seminerleri" 36 | select_panelist: "Kullanıcı adı veya e-posta" 37 | add_panelist: "Panelist ekle" 38 | nonzoom_details: "Geçmiş Web Semineri Meta Verileri" 39 | host: "Ev sahibi" 40 | select_host: "Kullanıcı adı veya e-posta" 41 | title_date: "Başlık ve Tarih" 42 | hosted_by: "Organize eden:" 43 | countdown_label: "Etkinliğe kalan süre:" 44 | days: "Gün" 45 | hours: "Saat" 46 | mins: "Dk" 47 | secs: "Sn" 48 | no_panelists: "Bu web seminerinin ilişkili panelisti yok. Panelist eklemek için aşağıdaki formu kullanın." 49 | no_panelists_preview: "Bu web seminerinin ilişkili panelisti yok. Daha sonra ekleyebilirsiniz." 50 | error: "Hata oluştu, lütfen tekrar deneyin. " 51 | join_sdk: "Hemen Katılın!" 52 | cancel_registration: "Kaydı İptal Et" 53 | no_registration_required: 'Bu web semineri, şu anda desteklenmeyen Zoom''da "Kayıt gerekli" olarak işaretlenmiştir. Lütfen web semineri ayarlarını düzenleyin ve tekrar deneyin.' 54 | webinar_existing_topic: "Bu web semineri zaten bir konuyla ilişkilendirilmiş. Konu Kimliği: %{topic_id}" 55 | webinar_ended: "Bu etkinlik sona erdi." 56 | webinar_recording: "Web Semineri Kaydı Ekle" 57 | webinar_recording_description: "Bu web semineri için video kaydının URL'sini girin. " 58 | show_recording: "Tekrar oynat" 59 | add_to_google_calendar: "Google Takvim'e ekle" 60 | add_to_outlook: "Outlook'a ekle" 61 | add_to_calendar: "Takvim'e ekle" 62 | webinar_footnote: "Etkinliği yapamıyor musunuz? Kaydolun ve size etkinlik sonrası bir özet gönderelim." 63 | webinar_logged_in_users_only: "Web semineri kaydına yalnızca oturum açmış kullanıcılar erişebilir." 64 | add_past_webinar: "Geçmiş web seminerini ekle" 65 | past_label: "Geçmiş web semineri başlığı" 66 | past_date: "Geçmiş web semineri tarihi" 67 | -------------------------------------------------------------------------------- /config/locales/client.ug.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | ug: 8 | js: 9 | zoom: 10 | remove: "چىقىرىۋەت" 11 | webinar_picker: 12 | create: "قوش" 13 | register: "خەتلەت" 14 | hours: "سائەت" 15 | -------------------------------------------------------------------------------- /config/locales/client.uk.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | uk: 8 | js: 9 | zoom: 10 | remove: "Вилучити" 11 | webinar_picker: 12 | create: "Додати" 13 | register: "Зареєструватися" 14 | hours: "годин" 15 | -------------------------------------------------------------------------------- /config/locales/client.ur.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | ur: 8 | js: 9 | zoom: 10 | remove: "خارج کریں" 11 | webinar_picker: 12 | create: "اضافہ کریں" 13 | register: "رجسٹر" 14 | hours: "گھنٹے" 15 | -------------------------------------------------------------------------------- /config/locales/client.vi.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | vi: 8 | js: 9 | zoom: 10 | remove: "Xoá" 11 | webinar_picker: 12 | create: "Thêm" 13 | register: "Đăng ký" 14 | hours: "Tiếng" 15 | -------------------------------------------------------------------------------- /config/locales/client.zh_CN.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | zh_CN: 8 | admin_js: 9 | admin: 10 | site_settings: 11 | categories: 12 | discourse_zoom: "Discourse Zoom" 13 | site_settings: 14 | zoom_enabled: "Zoom 插件已启用" 15 | zoom_send_reminder_minutes_before_webinar: "您希望在网络研讨会开始前多少分钟向参与者发送提醒通知?如果不想发送通知,则设置为空或 0。" 16 | js: 17 | zoom: 18 | remove: "移除" 19 | remove_webinar: "移除网络研讨会" 20 | edit_webinar: "编辑网络研讨会" 21 | add_webinar: "添加网络研讨会" 22 | confirm_remove: "确定要移除此网络研讨会吗?" 23 | toggle_edit_webinar_button: "编辑网络研讨会" 24 | webinar_picker: 25 | webinar_id: "网络研讨会 ID" 26 | title: "添加网络研讨会" 27 | create: "添加" 28 | button: "Zoom 网络研讨会" 29 | clear: "选择其他网络研讨会" 30 | register: "注册" 31 | registered: "已注册" 32 | panelists: "小组成员" 33 | attendees: "参与者" 34 | no_user_webinars: "您尚未注册任何网络研讨会。" 35 | webinars_title: "网络研讨会" 36 | select_panelist: "用户名或电子邮箱" 37 | add_panelist: "添加小组成员" 38 | nonzoom_details: "过去的网络研讨会元数据" 39 | host: "主持人" 40 | select_host: "用户名或电子邮箱" 41 | title_date: "标题和日期" 42 | hosted_by: "主办方:" 43 | countdown_label: "活动开始倒计时:" 44 | days: "天" 45 | hours: "小时" 46 | mins: "分钟" 47 | secs: "秒" 48 | no_panelists: "此网络研讨会没有相关的小组成员。请使用以下表格添加小组成员。" 49 | no_panelists_preview: "此网络研讨会没有相关的小组成员。您可以稍后添加。" 50 | error: "出错了,请重试。" 51 | join_sdk: "立即加入!" 52 | cancel_registration: "取消注册" 53 | no_registration_required: '此网络研讨会在 Zoom 中标记为“需要注册”,目前不受支持。请编辑网络研讨会设置,然后重试。' 54 | webinar_existing_topic: "此网络研讨会已与某个话题相关联。话题 ID:%{topic_id}" 55 | webinar_ended: "此活动现已结束。" 56 | webinar_recording: "添加网络研讨会录像" 57 | webinar_recording_description: "输入此网络研讨会视频录像的 URL。" 58 | show_recording: "再次播放" 59 | add_to_google_calendar: "添加到 Google 日历" 60 | add_to_outlook: "添加到 Outlook" 61 | add_to_calendar: "添加到日历" 62 | webinar_footnote: "无法参加活动?请注册,然后我们将向您发送活动后回顾。" 63 | webinar_logged_in_users_only: "只有登录用户才能注册网络研讨会。" 64 | add_past_webinar: "添加过去的网络研讨会" 65 | past_label: "过去的网络研讨会标题" 66 | past_date: "过去的网络研讨会日期" 67 | -------------------------------------------------------------------------------- /config/locales/client.zh_TW.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | zh_TW: 8 | js: 9 | zoom: 10 | remove: "移除" 11 | webinar_picker: 12 | create: "加入" 13 | register: "註冊" 14 | hours: "小時" 15 | -------------------------------------------------------------------------------- /config/locales/server.ar.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | ar: 8 | site_settings: 9 | zoom_sdk_key: "مفتاح SDK أو معرِّف العميل (ضمن علامة التبويب \"بيانات اعتماد التطبيق\")" 10 | zoom_sdk_secret: "مفتاح SDK أو سر العميل (ضمن علامة التبويب \"بيانات اعتماد التطبيق\")" 11 | zoom_s2s_account_id: "معرِّف الحساب لتطبيق Zoom Server to Server oAuth (ضمن علامة التبويب \"بيانات اعتماد التطبيق\")." 12 | zoom_s2s_client_id: "معرِّف العميل لتطبيق Zoom Server to Server oAuth (ضمن علامة التبويب \"بيانات الاعتماد\")." 13 | zoom_s2s_client_secret: "سر العميل لتطبيق Zoom Server to Server oAuth (ضمن علامة التبويب \"بيانات الاعتماد\")." 14 | s2s_oauth_token: "رمز Server to Server OAuth. سيتم تعبئته تلقائيًا" 15 | zoom_webhooks_secret_token: "الرمز السري لاشتراكات الأحداث في تطبيق Zoom Server to Server oAuth (علامة تبويب \"الميزات\"). ويُستخدَم لتحديث البيانات الوصفية للحدث عند إجراء تغييرات في واجهة المستخدم لبرنامج Zoom." 16 | zoom_host_title_override: "أدخل اسمًا مخصصًا لحقل المستخدم لعرض قيمته بدلًا من لقب المستخدم أسفل اسم المستخدم للمضيف." 17 | zoom_send_user_id: "أرسل معرِّف Discourse الداخلي للمستخدم إلى Zoom عند الانضمام إلى ندوة عبر الإنترنت. (اختياري)" 18 | zoom_display_attendees: "قائمة العرض لحاضري الحدث." 19 | zoom_join_x_mins_before_start: "التبديل إلى زر \"الانضمام\" في واجهة المستخدم قبل x دقيقة من بدء الندوة عبر الإنترنت. (تشير القيمة 0 إلى أنه سيتم تبديل الزر بمجرد بدء الحدث، بغض النظر عن وقت البدء)" 20 | zoom_enable_sdk_fallback: "عرض رابط إلى Zoom إذا فشل الانضمام إلى الحدث (على سبيل المثال، إذا كانت خدمة Web SDK لبرنامج Zoom متوقفة)." 21 | zoom_use_join_url: "جعل زر \"انضم الآن\" ينتقل بالمستخدمين إلى عنوان URL الذي يوفره Zoom." 22 | discourse_zoom_plugin_verbose_logging: "تفعيل التسجيل المطوَّل للمكوِّن الإضافي Zoom" 23 | errors: 24 | models: 25 | webinar: 26 | attributes: 27 | zoom_id: 28 | webinar_in_use: "هناك موضوع آخر مرتبط بالفعل بهذه الندوة عبر الإنترنت." 29 | zoom_plugin_errors: 30 | s2s_oauth_authorization: "تطبيق OAuth غير مصرَّح له بإجراء هذا الطلب" 31 | meeting_not_found: "لم يتم العثور على الاجتماع أو انتهت صلاحيته" 32 | contact_system_admin: "خطة الندوة عبر الإنترنت مطلوبة لاشتراك Zoom الخاص بهذا الموقع. يُرجى التواصل مع المسؤولين لديك." 33 | dashboard: 34 | problem: 35 | s2s_webinar_subscription: "المكوِّن الإضافي لبرنامج Zoom: خطة الندوات عبر الإنترنت مفقودة. يجب عليك الاشتراك في خطة الندوات عبر الإنترنت للوصول إلى هذه الميزة" 36 | system_messages: 37 | webinar_reminder: 38 | title: "ندوة عبر الإنترنت قادمة" 39 | subject_template: "هناك ندوة عبر الإنترنت على وشك البدء" 40 | text_body_template: | 41 | مرحبًا، 42 | 43 | هذه رسالة آلية من %{site_name} لإخبارك بأن هناك ندوة عبر الإنترنت أنت مسجلٌ بها على وشك البدء. 44 | 45 | <%{url}> 46 | -------------------------------------------------------------------------------- /config/locales/server.be.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | be: 8 | -------------------------------------------------------------------------------- /config/locales/server.bg.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | bg: 8 | -------------------------------------------------------------------------------- /config/locales/server.bs_BA.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | bs_BA: 8 | -------------------------------------------------------------------------------- /config/locales/server.ca.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | ca: 8 | -------------------------------------------------------------------------------- /config/locales/server.cs.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | cs: 8 | -------------------------------------------------------------------------------- /config/locales/server.da.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | da: 8 | -------------------------------------------------------------------------------- /config/locales/server.de.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | de: 8 | site_settings: 9 | zoom_sdk_key: "SDK-Schlüssel oder Client-ID (in der Registerkarte „App-Anmeldedaten“)" 10 | zoom_sdk_secret: "SDK-Geheimnis oder Client-Geheimnis (in der Registerkarte „App-Anmeldedaten“)" 11 | zoom_s2s_account_id: "Konto-ID für deine Zoom-Server-zu-Server-oAuth-App (in der Registerkarte „App-Anmeldedaten“)." 12 | zoom_s2s_client_id: "Client-ID für deine Zoom-Server-zu-Server-oAuth-App (in der Registerkarte „Anmeldedaten“)." 13 | zoom_s2s_client_secret: "Client-Geheimnis für deine Zoom-Server-zu-Server-oAuth-App (in der Registerkarte „Anmeldedaten“)." 14 | s2s_oauth_token: "Server-zu-Server-OAuth-Token. Wird automatisch ausgefüllt" 15 | zoom_webhooks_secret_token: "Geheimes Token für die Event-Abonnements deiner Zoom-Server-zu-Server-oAuth-App (unter der Registerkarte „Funktionen“). Wird verwendet, um die Event-Metadaten zu aktualisieren, wenn Änderungen über die Zoom-Benutzeroberfläche vorgenommen werden." 16 | zoom_host_title_override: "Gib einen benutzerdefinierten Benutzerfeldnamen ein, damit sein Wert anstelle des Benutzertitels unter dem Benutzernamen des Gastgebers angezeigt wird." 17 | zoom_send_user_id: "Interne Discourse-ID des Benutzers bei Webinar-Beitritt an Zoom schicken (optional)." 18 | zoom_display_attendees: "Liste der Event-Teilnehmer anzeigen." 19 | zoom_join_x_mins_before_start: "Auf der Benutzeroberfläche x Minuten vor Beginn des Webinars zur Schaltfläche „Teilnehmen“ umschalten (0 bedeutet, dass die Schaltfläche umgeschaltet wird, sobald das Event gestartet wird, unabhängig von der Startzeit)." 20 | zoom_enable_sdk_fallback: "Einen Link zu Zoom anzeigen, wenn der Beitritt zu einem Event fehlschlägt (z. B. wenn der Web-SDK-Dienst von Zoom ausgefallen ist)." 21 | zoom_use_join_url: "Die Schaltfläche „Jetzt beitreten“ führt die Benutzer zu der von Zoom bereitgestellten Beitrittsurl." 22 | discourse_zoom_plugin_verbose_logging: "Ausführliche Protokollierung für das Zoom-Plug-in aktivieren" 23 | errors: 24 | models: 25 | webinar: 26 | attributes: 27 | zoom_id: 28 | webinar_in_use: "Mit diesem Webinar ist bereits ein anderes Thema verknüpft." 29 | zoom_plugin_errors: 30 | s2s_oauth_authorization: "Die OAuth-App ist nicht berechtigt, diese Anfrage zu stellen" 31 | meeting_not_found: "Meeting nicht gefunden oder abgelaufen" 32 | contact_system_admin: "Der Webinar-Tarif ist für das Zoom-Abonnement dieser Website erforderlich. Bitte kontaktiere deine Administratoren." 33 | dashboard: 34 | problem: 35 | s2s_webinar_subscription: "Zoom-Plug-in: Webinar-Tarif fehlt. Du musst den Webinar-Tarif abonnieren, um Zugang zu dieser Funktion zu haben" 36 | system_messages: 37 | webinar_reminder: 38 | title: "Kommendes Webinar" 39 | subject_template: "Ein Webinar beginnt gleich" 40 | text_body_template: | 41 | Hallo, 42 | 43 | dies ist eine automatische Nachricht von %{site_name}, um dich darüber zu informieren, dass ein Webinar, für das du dich registriert hast, in Kürze beginnt. 44 | 45 | <%{url}> 46 | -------------------------------------------------------------------------------- /config/locales/server.el.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | el: 8 | -------------------------------------------------------------------------------- /config/locales/server.en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | site_settings: 3 | zoom_sdk_key: "SDK key or Client ID (under the App Credentials tab)" 4 | zoom_sdk_secret: "SDK secret or Client secret (under the App Credentials tab)" 5 | zoom_s2s_account_id: "Account ID for your Zoom Server to Server oAuth app (under the App Credentials tab)." 6 | zoom_s2s_client_id: "Client ID for your Zoom Server to Server oAuth app (under the Credentials tab)." 7 | zoom_s2s_client_secret: "Client Secret for your Zoom Server to Server oAuth app (under the Credentials tab)." 8 | s2s_oauth_token: "Server to Server OAuth token. This will be filled automatically" 9 | zoom_webhooks_secret_token: "Secret token for your Zoom Server to Server oAuth app's Event Subscriptions (under the Feature tab). Used to update event metadata when changes are made in the Zoom UI." 10 | zoom_host_title_override: "Enter a custom user field name to displays its value instead of the user title below the host's username." 11 | zoom_send_user_id: "Send the user's internal Discourse ID to Zoom when joining a webinar. (optional)" 12 | zoom_display_attendees: "Display list of event attendees." 13 | zoom_join_x_mins_before_start: "Switch to Join button in the UI x minutes before the start of the webinar. (0 means button will be switched as soon as event is started, regardless of start time)" 14 | zoom_enable_sdk_fallback: "Display a link to Zoom if joining event fails (for example, if Zoom's Web SDK service is down)." 15 | zoom_use_join_url: "Have the Join Now button take users to the join url provided by Zoom." 16 | discourse_zoom_plugin_verbose_logging: "Enable verbose logging for Zoom plugin" 17 | activerecord: 18 | errors: 19 | models: 20 | webinar: 21 | attributes: 22 | zoom_id: 23 | webinar_in_use: "Another topic is already associated with this webinar." 24 | 25 | zoom_plugin_errors: 26 | s2s_oauth_authorization: "OAuth app is not authorized to make this request" 27 | meeting_not_found: "Meeting not found or has expired" 28 | contact_system_admin: "The Webinar plan is required for this site's Zoom subscription. Please contact your administrators." 29 | 30 | dashboard: 31 | problem: 32 | s2s_webinar_subscription: "Zoom Plugin: Webinar plan is missing. You must subscribe to the webinar plan to have access to this feature" 33 | 34 | system_messages: 35 | webinar_reminder: 36 | title: "Upcoming Webinar" 37 | subject_template: "A webinar is about to begin" 38 | text_body_template: | 39 | Hello, 40 | 41 | This is an automated message from %{site_name} to let you know that a webinar that you are registered for is about to begin. 42 | 43 | <%{url}> 44 | -------------------------------------------------------------------------------- /config/locales/server.en_GB.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | en_GB: 8 | -------------------------------------------------------------------------------- /config/locales/server.es.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | es: 8 | site_settings: 9 | zoom_sdk_key: "Clave SDK o ID de cliente (en la pestaña Credenciales de la aplicación)" 10 | zoom_sdk_secret: "Secreto del SDK o Secreto del cliente (en la pestaña Credenciales de la aplicación)" 11 | zoom_s2s_account_id: "ID de cuenta para tu aplicación Zoom Server to Server oAuth (en la pestaña Credenciales de la aplicación)." 12 | zoom_s2s_client_id: "ID de cliente para tu aplicación Zoom Server to Server oAuth (en la pestaña Credenciales)." 13 | zoom_s2s_client_secret: "Secreto de cliente para tu aplicación Zoom Server to Server oAuth (en la pestaña Credenciales)." 14 | s2s_oauth_token: "Token de Server to Server OAuth. Esto se rellenará automáticamente" 15 | zoom_webhooks_secret_token: "Token secreto para las Suscripciones a aventos de tu aplicación Zoom Server to Server oAuth (en la pestaña Características). Se utiliza para actualizar los metadatos de los eventos cuando se realizan cambios en la interfaz de usuario de Zoom." 16 | zoom_host_title_override: "Introduce un nombre de campo de usuario personalizado para mostrar su valor en lugar del título del usuario debajo del nombre de usuario del anfitrión." 17 | zoom_send_user_id: "Envía el ID interno de Discourse del usuario a Zoom cuando se una a un seminario web. (opcional)" 18 | zoom_display_attendees: "Mostrar lista de asistentes al evento." 19 | zoom_join_x_mins_before_start: "Cambiar al botón Unirse en la interfaz de usuario x minutos antes del inicio del seminario web. (0 significa que el botón se activará en cuanto se inicie el evento, independientemente de la hora de inicio)" 20 | zoom_enable_sdk_fallback: "Mostrar un enlace a Zoom si no es posible unirse al evento (por ejemplo, si el servicio Web SDK de Zoom no funciona)." 21 | zoom_use_join_url: "Hacer que el botón Unirse ahora lleve a los usuarios a la URL de unirse proporcionada por Zoom." 22 | discourse_zoom_plugin_verbose_logging: "Habilitar el registro detallado para el plugin de Zoom" 23 | errors: 24 | models: 25 | webinar: 26 | attributes: 27 | zoom_id: 28 | webinar_in_use: "Ya hay otro tema asociado a este seminario web." 29 | zoom_plugin_errors: 30 | s2s_oauth_authorization: "La aplicación OAuth no está autorizada a realizar esta solicitud" 31 | meeting_not_found: "No se encontró la reunión o ha caducado" 32 | contact_system_admin: "Se requiere el plan Webinar para la suscripción Zoom de este sitio. Contacta con tus administradores." 33 | dashboard: 34 | problem: 35 | s2s_webinar_subscription: "Complemento Zoom: Falta el plan Webinar. Debes suscribirte al plan Webinar para tener acceso a esta característica" 36 | system_messages: 37 | webinar_reminder: 38 | title: "Próximo seminario web" 39 | subject_template: "Un seminario web está a punto de comenzar" 40 | text_body_template: | 41 | Hola: 42 | 43 | Este es un mensaje automático de %{site_name} para informarte de que está a punto de comenzar un seminario web al que te has inscrito. 44 | 45 | <%{url}> 46 | -------------------------------------------------------------------------------- /config/locales/server.et.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | et: 8 | -------------------------------------------------------------------------------- /config/locales/server.fa_IR.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | fa_IR: 8 | -------------------------------------------------------------------------------- /config/locales/server.fi.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | fi: 8 | site_settings: 9 | zoom_sdk_key: "SDK key tai Client ID (App Credentials -välilehdessä)" 10 | zoom_sdk_secret: "SDK secret tai Client secret (App Credentials -välilehdessä)" 11 | zoom_s2s_account_id: "Zoomin Server to Server oAuth -sovelluksen tilitunnus (Account ID) (App Credentials -välilehdessä)." 12 | zoom_s2s_client_id: "Zoomin Server to Server oAuth -sovelluksen asiakastunnus (Client ID) (App Credentials -välilehdessä)." 13 | zoom_s2s_client_secret: "Zoomin Server to Server oAuth -sovelluksen Client Secret (App Credentials -välilehdessä)." 14 | s2s_oauth_token: "Server to Server oAuth -tunnus. Tämä täytetään automaattisesti." 15 | zoom_webhooks_secret_token: "Zoomin Server to Server oAuth -sovelluksen tapahtumatilausten salatunnus (secret token) (Feature-välilehdessä). Tätä käytetään tapahtuman metatietojen päivittämiseen, kun Zoomin käyttöliittymässä tehdään muutoksia." 16 | zoom_host_title_override: "Anna mukautettu käyttäjäkentän nimi näyttääksesi sen arvon isännän käyttäjänimen alapuolella olevan käyttäjän nimen sijaan." 17 | zoom_send_user_id: "Lähetä käyttäjän sisäinen Discourse-tunnus Zoomiin webinaariin liityttäessä (valinnainen)." 18 | zoom_display_attendees: "Näytä luettelo tapahtuman osallistujista." 19 | zoom_join_x_mins_before_start: "Vaihda käyttöliittymässä Liity-painikkeeseen x minuuttia ennen webinaarin alkua. (0 tarkoittaa, että painike vaihdetaan heti kun tapahtuma alkaa alkamisajasta riippumatta)." 20 | zoom_enable_sdk_fallback: "Näytä linkki Zoomiin, jos tapahtumaan liittyminen epäonnistuu (esimerkiksi jos Zoomin Web SDK -palvelu ei toimi)." 21 | zoom_use_join_url: "Laita Liity nyt -painike viemään käyttäjät liittymään Zoomin tarjoamaan URL-osoitteeseen." 22 | discourse_zoom_plugin_verbose_logging: "Ota Zoom-lisäosan monisanainen lokikirjaus käyttöön" 23 | errors: 24 | models: 25 | webinar: 26 | attributes: 27 | zoom_id: 28 | webinar_in_use: "Toinen ketju on jo liitetty tähän webinaariin." 29 | zoom_plugin_errors: 30 | s2s_oauth_authorization: "OAuth-sovelluksella ei ole valtuuksia tehdä tätä pyyntöä" 31 | meeting_not_found: "Kokousta ei löydy tai se on vanhentunut" 32 | contact_system_admin: "Webinaarisopimus vaaditaan tämän sivuston Zoom-tilaukseen. Ota yhteyttä ylläpitäjiisi." 33 | dashboard: 34 | problem: 35 | s2s_webinar_subscription: "Zoom-lisäosa: webinaarisopimus puuttuu. Sinun täytyy tilata webinaarisopimus, jotta voit käyttää tätä ominaisuutta" 36 | system_messages: 37 | webinar_reminder: 38 | title: "Tuleva webinaari" 39 | subject_template: "Webinaari on alkamassa" 40 | text_body_template: | 41 | Hei, 42 | 43 | %{site_name} lähetti sinulle tämän automaattisen viestin ilmoittaakseen, että webinaari, johon olet rekisteröitynyt, on alkamassa. 44 | 45 | <%{url} 46 | -------------------------------------------------------------------------------- /config/locales/server.fr.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | fr: 8 | site_settings: 9 | zoom_sdk_key: "Clé SDK ou ID client (sous l'onglet Informations d'identification de l'application)" 10 | zoom_sdk_secret: "Secret SDK ou secret client (sous l'onglet Informations d'identification de l'application)" 11 | zoom_s2s_account_id: "ID de compte pour votre application Zoom Server to Server oAuth (sous l'onglet Identifiants de l'application)." 12 | zoom_s2s_client_id: "ID de client pour votre application Zoom Server to Server oAuth (sous l'onglet Informations d'identification)." 13 | zoom_s2s_client_secret: "Secret de client pour votre application Zoom Server to Server oAuth (sous l'onglet Informations d'identification)." 14 | s2s_oauth_token: "Jeton OAuth de serveur à serveur. Celui-ci sera rempli automatiquement" 15 | zoom_webhooks_secret_token: "Jeton secret pour les inscriptions aux événements de votre application Zoom Server to Server oAuth (sous l'onglet Fonctionnalité). Utilisé pour mettre à jour les métadonnées d'événement lorsque des modifications sont apportées dans l'interface utilisateur de Zoom." 16 | zoom_host_title_override: "Saisir un nom de champ utilisateur personnalisé pour afficher sa valeur au lieu du titre de l'utilisateur sous le nom d'utilisateur de l'hôte." 17 | zoom_send_user_id: "Envoyer l'identifiant Discourse interne de l'utilisateur à Zoom lorsque vous participez à un webinaire. (facultatif)" 18 | zoom_display_attendees: "Afficher la liste des participants à l'événement." 19 | zoom_join_x_mins_before_start: "Passer au bouton Rejoindre dans l'interface utilisateur x minutes avant le début du webinaire. (0 signifie que le bouton sera affiché dès le début de l'événement, quelle que soit l'heure de début)" 20 | zoom_enable_sdk_fallback: "Afficher un lien vers Zoom si la participation à l'événement échoue (par exemple, si le service Web SDK de Zoom est hors service)." 21 | zoom_use_join_url: "Faire en sorte que le bouton Rejoindre maintenant emmène les utilisateurs à l'adresse URL de connexion fournie par Zoom." 22 | discourse_zoom_plugin_verbose_logging: "Activer la journalisation détaillée pour l'extension Zoom" 23 | errors: 24 | models: 25 | webinar: 26 | attributes: 27 | zoom_id: 28 | webinar_in_use: "Un autre sujet est déjà associé à ce webinaire." 29 | zoom_plugin_errors: 30 | s2s_oauth_authorization: "L'application OAuth n'est pas autorisée à effectuer cette demande" 31 | meeting_not_found: "Réunion introuvable ou expirée" 32 | contact_system_admin: "Le forfait de webinaire est requis pour l'abonnement Zoom de ce site. Veuillez contacter vos administrateurs." 33 | dashboard: 34 | problem: 35 | s2s_webinar_subscription: "Extension Zoom : il manque un forfait de webinaire. Vous devez souscrire à un forfait de webinaire pour avoir accès à cette fonctionnalité" 36 | system_messages: 37 | webinar_reminder: 38 | title: "Webinaire à venir" 39 | subject_template: "Un webinaire est sur le point de commencer" 40 | text_body_template: | 41 | Bonjour, 42 | 43 | Il s'agit d'un message automatique envoyé par %{site_name} pour vous informer qu'un webinaire auquel vous êtes inscrit(e) est sur le point de commencer. 44 | 45 | <%{url} 46 | -------------------------------------------------------------------------------- /config/locales/server.gl.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | gl: 8 | -------------------------------------------------------------------------------- /config/locales/server.he.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | he: 8 | site_settings: 9 | zoom_sdk_key: "מפתח SDK או מזהה לקוח (תחת הלשונית פרטי גישה ליישומון)" 10 | zoom_sdk_secret: "סוד SDK או סוד לקוח (תחת הלשונית אישורי גישה ליישומון)" 11 | s2s_oauth_token: "אסימון OAuth של שרת לשרת. יתמלא אוטומטית" 12 | zoom_send_user_id: "שלח את מזהה המשתמש הפנימי של Discourse לזום בעת הצטרפות לובינר. (אופציונלי)" 13 | zoom_display_attendees: "הצגת רשימת המשתתפים באירוע." 14 | zoom_enable_sdk_fallback: "הצגת קישור לזום אם ההצטרפות לאירוע נכשלת (למשל, אם שירות ה־SDK המקוון של זום מושבת)." 15 | zoom_use_join_url: "להגדיר את כפתור הצטרפות כעת/Join Now שיעביר את המשתמשים לכתובת ההצטרפות לכתובת שסופקה על ידי Zoom" 16 | discourse_zoom_plugin_verbose_logging: "הפעלת תיעוד מפורט לתוסף Zoom" 17 | errors: 18 | models: 19 | webinar: 20 | attributes: 21 | zoom_id: 22 | webinar_in_use: "נושא אחר כבר משויך לוובינר הזה." 23 | zoom_plugin_errors: 24 | s2s_oauth_authorization: "יישום OAuth לא מורשה לבצע את הבקשה הזאת" 25 | meeting_not_found: "הישיבה לא נמצאה או שתוקפה פג" 26 | contact_system_admin: "תוכנית הוובינר נחוצה למינוי ה־Zoom של האתר הזה. נא ליצור קשר עם הנהלת המערכת שלך." 27 | dashboard: 28 | problem: 29 | s2s_webinar_subscription: "תוסף Zoom: תוכנית ובינר חסרה. חובה להירשם לוובינר כדי לגשת ליכולת הזאת" 30 | system_messages: 31 | webinar_reminder: 32 | title: "הוובינר הקרוב" 33 | subject_template: "ובינר עומד להתחיל" 34 | text_body_template: | 35 | שלום, 36 | 37 | זו הודעה אוטומטית מהאתר %{site_name} כדי ליידע אותך שוובינר שנרשמת אליו עומד להתחיל. 38 | 39 | <%{url}> 40 | -------------------------------------------------------------------------------- /config/locales/server.hr.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | hr: 8 | -------------------------------------------------------------------------------- /config/locales/server.hu.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | hu: 8 | -------------------------------------------------------------------------------- /config/locales/server.hy.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | hy: 8 | -------------------------------------------------------------------------------- /config/locales/server.id.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | id: 8 | -------------------------------------------------------------------------------- /config/locales/server.it.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | it: 8 | site_settings: 9 | zoom_sdk_key: "Chiave SDK o ID client (nella scheda Credenziali app)" 10 | zoom_sdk_secret: "Chiave SDK o Segreto client (nella scheda Credenziali app)" 11 | zoom_s2s_account_id: "ID account per l'app Zoom Server to Server oAuth (nella scheda Credenziali app)." 12 | zoom_s2s_client_id: "ID client per l'app Zoom Server to Server oAuth (nella scheda Credenziali)." 13 | zoom_s2s_client_secret: "Segreto client per l'app Zoom Server to Server oAuth (nella scheda Credenziali)." 14 | s2s_oauth_token: "Token Server to Server OAuth. Questo campo verrà riempito automaticamente" 15 | zoom_webhooks_secret_token: "Token segreto per gli abbonamenti agli eventi dell'app Zoom Server to Server oAuth (nella scheda Funzionalità). Utilizzato per aggiornare i metadati dell'evento quando vengono apportate modifiche nell'interfaccia utente di Zoom." 16 | zoom_host_title_override: "Inserisci un nome campo utente personalizzato per visualizzarne il valore anziché il titolo utente sotto il nome utente dell'ospite." 17 | zoom_send_user_id: "Invia l'ID Discourse interno dell'utente a Zoom quando partecipi a un webinar. (opzionale)" 18 | zoom_display_attendees: "Visualizza l'elenco dei partecipanti all'evento." 19 | zoom_join_x_mins_before_start: "Passa al pulsante Partecipa nell'interfaccia utente x minuti prima dell'inizio del webinar. (0 significa che il pulsante verrà commutato non appena l'evento viene avviato, indipendentemente dall'ora di inizio)" 20 | zoom_enable_sdk_fallback: "Visualizza un collegamento a Zoom se l'evento di partecipazione non riesce (ad esempio, se il servizio web SDK di Zoom non è attivo)." 21 | zoom_use_join_url: "Fai in modo che il pulsante Iscriviti ora porti gli utenti all'URL di accesso fornito da Zoom." 22 | discourse_zoom_plugin_verbose_logging: "Abilita la registrazione dettagliata per il plug-in Zoom" 23 | errors: 24 | models: 25 | webinar: 26 | attributes: 27 | zoom_id: 28 | webinar_in_use: "Un altro argomento è già associato a questo webinar." 29 | zoom_plugin_errors: 30 | s2s_oauth_authorization: "L'app OAuth non è autorizzata a effettuare questa richiesta" 31 | meeting_not_found: "Riunione non trovata o scaduta" 32 | contact_system_admin: "Il piano Webinar è obbligatorio per l'abbonamento Zoom di questo sito. Contatta i tuoi amministratori." 33 | dashboard: 34 | problem: 35 | s2s_webinar_subscription: "Plugin Zoom: manca il piano Webinar. Devi abbonarti al piano Webinar per avere accesso a questa funzionalità" 36 | system_messages: 37 | webinar_reminder: 38 | title: "Prossimo webinar" 39 | subject_template: "Sta per iniziare un webinar" 40 | text_body_template: | 41 | Ciao, 42 | 43 | Questo è un messaggio automatico da %{site_name} per informarti che sta per iniziare un webinar per il quale hai effettuato la registrazione. 44 | 45 | %{url} 46 | -------------------------------------------------------------------------------- /config/locales/server.ja.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | ja: 8 | site_settings: 9 | zoom_sdk_key: "SDK キーまたはクライアント ID (アプリの資格情報タブ)" 10 | zoom_sdk_secret: "SDK シークレットまたはクライアントシークレット (アプリの資格情報タブ)" 11 | zoom_s2s_account_id: "Zoom Server to Server oAuth アプリのアカウント ID (アプリの資格情報タブ)" 12 | zoom_s2s_client_id: "Zoom Server to Server oAuth アプリのクライアント ID (資格情報タブ)" 13 | zoom_s2s_client_secret: "Zoom Server to Server oAuth アプリのクライアントシークレット (資格情報タブ)" 14 | s2s_oauth_token: "Server to Server OAuth トークン。これは自動的に入力されます。" 15 | zoom_webhooks_secret_token: "Zoom Server to Server OAuth アプリのイベントサブスクリプションのシークレットトークン (機能タブ)。Zoom UI で変更が適用されたときにイベントメタデータを更新するのに使用されます。" 16 | zoom_host_title_override: "ホストのユーザー名の下にユーザーのタイトルの代わりに表示する値のカスタムユーザーフィールド名を入力します。" 17 | zoom_send_user_id: "ウェビナーに参加する際に、ユーザーの内部 Discourse ID を Zoom に送信します。(オプション)" 18 | zoom_display_attendees: "イベント出席者のリストを表示します。" 19 | zoom_join_x_mins_before_start: "ウェビナー開始の x 分前に、UI を参加ボタンに切り替えます。(0 の場合、開始時間に関係なく、イベントが開始するとすぐにボタンが切り替えられます)" 20 | zoom_enable_sdk_fallback: "イベントの参加に失敗した場合 (Zoom の Web SDK サービスがダウンしている場合など)、Zoom へのリンクを表示します。" 21 | zoom_use_join_url: "今すぐ参加ボタンをクリックすると、Zoom が提供する参加 URL に移動します。" 22 | discourse_zoom_plugin_verbose_logging: "Zoom プラグインの詳細なログ記録を有効にする" 23 | errors: 24 | models: 25 | webinar: 26 | attributes: 27 | zoom_id: 28 | webinar_in_use: "このウェビナーには別のトピックがすでに関連付けられています。" 29 | zoom_plugin_errors: 30 | s2s_oauth_authorization: "OAuth アプリにはこのリクエストを行うための権限がありません" 31 | meeting_not_found: "会議は見つからないか期限切れです" 32 | contact_system_admin: "このサイトの Zoom サブスクリプションにはウェビナープランが必要です。管理者にお問い合わせください。" 33 | dashboard: 34 | problem: 35 | s2s_webinar_subscription: "Zoom プラグイン: ウェビナープランがありません。この機能を利用するには、ウェビナープランを購読する必要があります。" 36 | system_messages: 37 | webinar_reminder: 38 | title: "開催予定のウェビナー" 39 | subject_template: "ウェビナーはまもなく開始します" 40 | text_body_template: | 41 | こんにちは。 42 | 43 | これは %{site_name} からの自動メッセージです。登録済みのウェビナーがまもなく開始されることをお知らせします。 44 | 45 | <%{url}> 46 | -------------------------------------------------------------------------------- /config/locales/server.ko.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | ko: 8 | -------------------------------------------------------------------------------- /config/locales/server.lt.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | lt: 8 | -------------------------------------------------------------------------------- /config/locales/server.lv.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | lv: 8 | -------------------------------------------------------------------------------- /config/locales/server.nb_NO.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | nb_NO: 8 | -------------------------------------------------------------------------------- /config/locales/server.nl.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | nl: 8 | site_settings: 9 | zoom_sdk_key: "SDK-sleutel of client-ID (op het tabblad Appaanmeldgegevens)" 10 | zoom_sdk_secret: "SDK-geheim of clientgeheim (op het tabblad Appaanmeldgegevens)" 11 | zoom_s2s_account_id: "Account-ID voor je Zoom Server-to-Server oAuth-app (op het tabblad Appaanmeldgegevens)." 12 | zoom_s2s_client_id: "Client-ID voor je Zoom Server-to-Server oAuth-app (op het tabblad Aanmeldgegevens)." 13 | zoom_s2s_client_secret: "Clientgeheim voor je Zoom Server-to-Server oAuth-app (op het tabblad Aanmeldgegevens)." 14 | s2s_oauth_token: "Server-to-Server OAuth-token. Dit wordt automatisch ingevuld" 15 | zoom_webhooks_secret_token: "Geheim token voor de gebeurtenisabonnementen van je Zoom Server-to-Server OAuth-app (op het tabblad Functie). Wordt gebruikt om metagegevens van gebeurtenissen bij te werken wanneer er wijzigingen zijn in de Zoom-UI." 16 | zoom_host_title_override: "Voer een aangepaste gebruikersveldnaam in om de waarde ervan weer te geven in plaats van de gebruikerstitel onder de gebruikersnaam van de host." 17 | zoom_send_user_id: "Stuur de interne Discourse-ID van de gebruiker naar Zoom wanneer deze deelneemt aan een webinar (optioneel)" 18 | zoom_display_attendees: "Geef een lijst van evenementdeelnemers weer." 19 | zoom_join_x_mins_before_start: "Schakel x minuten voor aanvang van het webinar over naar de knop Deelnemen in de gebruikersinterface. (0 betekent dat de knop wordt omgeschakeld zodra het evenement begint, ongeacht de begintijd)" 20 | zoom_enable_sdk_fallback: "Geef een link naar Zoom weer als deelname aan een evenement mislukt (bijvoorbeeld als de Web SDK-service van Zoom niet beschikbaar is)." 21 | zoom_use_join_url: "Laat de knop Nu deelnemen gebruikers naar de deelname-URL van Zoom leiden." 22 | discourse_zoom_plugin_verbose_logging: "Uitgebreide logging inschakelen voor Zoom-plug-in" 23 | errors: 24 | models: 25 | webinar: 26 | attributes: 27 | zoom_id: 28 | webinar_in_use: "Er is al een ander topic aan dit webinar gekoppeld." 29 | zoom_plugin_errors: 30 | s2s_oauth_authorization: "De OAuth-app is niet geautoriseerd om dit verzoek te doen" 31 | meeting_not_found: "Vergadering niet gevonden of verlopen" 32 | contact_system_admin: "Het Webinar-abonnement is vereist voor het Zoom-abonnement op deze site. Neem contact op met je beheerders." 33 | dashboard: 34 | problem: 35 | s2s_webinar_subscription: "Zoom-plug-in: Webinar-abonnement ontbreekt. Je moet een Webinar-abonnement nemen voor toegang tot deze functie." 36 | system_messages: 37 | webinar_reminder: 38 | title: "Aankomend webinar" 39 | subject_template: "Er staat een webinar op het punt van beginnen" 40 | text_body_template: | 41 | Hallo, 42 | 43 | Dit is een automatisch bericht van %{site_name} om je te laten weten dat een webinar waarvoor je je hebt geregistreerd op het punt staat te beginnen. 44 | 45 | <%{url} 46 | -------------------------------------------------------------------------------- /config/locales/server.pl_PL.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | pl_PL: 8 | site_settings: 9 | zoom_host_title_override: "Wprowadź niestandardową nazwę pola użytkownika, aby wyświetlić jej wartość zamiast tytułu użytkownika pod nazwą użytkownika prowadzącego." 10 | zoom_send_user_id: "Wyślij wewnętrzny identyfikator Discourse użytkownika do Zoom podczas dołączania do webinaru. (opcjonalnie)" 11 | zoom_display_attendees: "Wyświetl listę uczestników wydarzenia." 12 | zoom_join_x_mins_before_start: "Przełącz się na przycisk dołącz w interfejsie na x minut przed rozpoczęciem webinaru. (0 oznacza, że przycisk zostanie przełączony zaraz po rozpoczęciu wydarzenia, niezależnie od czasu rozpoczęcia)" 13 | zoom_use_join_url: "Niech przycisk dołącz teraz przeniesie użytkowników do adresu URL dołączenia dostarczonego przez Zoom." 14 | errors: 15 | models: 16 | webinar: 17 | attributes: 18 | zoom_id: 19 | webinar_in_use: "Inny temat jest już powiązany z tym webinarem." 20 | system_messages: 21 | webinar_reminder: 22 | title: "Nadchodzący webinar" 23 | subject_template: "Za chwilę rozpocznie się webinar" 24 | text_body_template: | 25 | Witaj, 26 | 27 | To jest automatyczna wiadomość od %{site_name} , aby poinformować Cię, że webinarium, na które jesteś zarejestrowany, wkrótce się rozpocznie. 28 | 29 | <%{url}> 30 | -------------------------------------------------------------------------------- /config/locales/server.pt.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | pt: 8 | -------------------------------------------------------------------------------- /config/locales/server.pt_BR.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | pt_BR: 8 | site_settings: 9 | zoom_sdk_key: "Chave SDK ou ID de cliente (na aba Credenciais do Aplicativo)" 10 | zoom_sdk_secret: "Segredo SDK ou ID secreto (na aba Credenciais do aplicativo)" 11 | zoom_s2s_account_id: "ID da conta do seu aplicativo Zoom Server to Server oAuth (na aba Credenciais do aplicativo)." 12 | zoom_s2s_client_id: "ID do cliente do seu aplicativo Zoom Server to Server oAuth (na aba Credenciais)." 13 | zoom_s2s_client_secret: "Cliente secreto do seu aplicativo Zoom Server to Server oAuth (na aba Credenciais)." 14 | s2s_oauth_token: "Token Server to Server OAuth. Preenchido automaticamente" 15 | zoom_webhooks_secret_token: "Token secreto para inscrições em eventos do aplicativo Zoom Server to Server oAuth (na aba Recursos). Usado para atualizar metadados de eventos quando forem realizadas alterações na IU do Zoom." 16 | zoom_host_title_override: "Insira um nome de campo de usuário(a) personalizado para mostrar seu valor em vez do título do usuário(a) abaixo do nome do(a) usuário(a) do host." 17 | zoom_send_user_id: "Enviar a ID interna do(a) usuário(a) do Discourse para o Zoom ao participar de um webinar (opcional)." 18 | zoom_display_attendees: "Exiba a lista de participantes do evento." 19 | zoom_join_x_mins_before_start: "Alterne para o botão de Participar na IU x minutos antes do início do webinar. (0 indica que o botão será trocado logo no começo do evento, seja qual for o horário de início.)" 20 | zoom_enable_sdk_fallback: "Exiba um link para o Zoom se não for possível participar do evento (por exemplo: se o serviço Web SDK do Zoom estiver desativado)." 21 | zoom_use_join_url: "Faça com que o botão de Participar leve os(as) usuários(as) para a URL de ingresso fornecida pelo Zoom." 22 | discourse_zoom_plugin_verbose_logging: "Ativar registro de diagnósticos detalhados do plugin do Zoom" 23 | errors: 24 | models: 25 | webinar: 26 | attributes: 27 | zoom_id: 28 | webinar_in_use: "Outro tópico já está associado a este webinar." 29 | zoom_plugin_errors: 30 | s2s_oauth_authorization: "O aplicativo OAuth não tem autorização para solicitar isto" 31 | meeting_not_found: "Reunião não encontrada ou expirada" 32 | contact_system_admin: "A assinatura do Zoom para este site requer o plano de Webinar. Entre em contato com \na administração." 33 | dashboard: 34 | problem: 35 | s2s_webinar_subscription: "Plugin Zoom: falta o plano de Webinar. É preciso fazer a assinatura do plano de webinar para acessar esse recurso" 36 | system_messages: 37 | webinar_reminder: 38 | title: "Próximo webinar" 39 | subject_template: "Um webinar está prestes a começar" 40 | text_body_template: | 41 | Olá, 42 | 43 | Esta é uma mensagem automática do %{site_name} para avisar que o webinar no qual você se inscreveu está prestes a começar. 44 | 45 | <%{url}> 46 | -------------------------------------------------------------------------------- /config/locales/server.ro.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | ro: 8 | -------------------------------------------------------------------------------- /config/locales/server.ru.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | ru: 8 | site_settings: 9 | zoom_sdk_key: "Ключ SDK или идентификатор клиента (на вкладке «Учетные данные приложения»)." 10 | zoom_sdk_secret: "Секретный код SDK или секретный код клиента (на вкладке «Учетные данные приложения»)." 11 | zoom_s2s_account_id: "Идентификатор учетной записи для межсерверной связи с Zoom по oAuth (на вкладке «Учетные данные приложения»)." 12 | zoom_s2s_client_id: "Идентификатор клиента для межсерверной связи с Zoom по oAuth (на вкладке «Учетные данные»)." 13 | zoom_s2s_client_secret: "Секретный код клиента для межсерверной связи с Zoom по oAuth (на вкладке «Учетные данные»)." 14 | s2s_oauth_token: "Токен для межсерверной связи по oAuth, будет заполнен автоматически" 15 | zoom_webhooks_secret_token: "Секретный токен для подписок на события приложения oAuth для межсерверной связи с Zoom (на вкладке «Функция»). Позволяет обновлять метаданные событий при внесении изменений в интерфейсе Zoom." 16 | zoom_host_title_override: "Введите настраиваемое название для поля имени пользователя, которое будет отображаться вместо названия роли пользователя под именем пользователя организатора." 17 | zoom_send_user_id: "Отправлять внутренний идентификатор Discourse пользователя в Zoom при подключении к вебинару (необязательно)." 18 | zoom_display_attendees: "Показать список зрителей события." 19 | zoom_join_x_mins_before_start: "Переключаться на кнопку «Присоединиться» в интерфейсе за указанное количество минут до начала вебинара. (0 — кнопка будет переключаться в момент начала события.)" 20 | zoom_enable_sdk_fallback: "Отображать ссылку на Zoom, если присоединиться к событию не удалось (например, если сервис Web SDK платформы Zoom не работает)." 21 | zoom_use_join_url: "Кнопка «Присоединиться» перенаправляет пользователей на URL-адрес входа, получаемый от Zoom." 22 | discourse_zoom_plugin_verbose_logging: "Включает ведение подробного журнала для плагина Zoom" 23 | errors: 24 | models: 25 | webinar: 26 | attributes: 27 | zoom_id: 28 | webinar_in_use: "С этим вебинаром уже связана другая тема." 29 | zoom_plugin_errors: 30 | s2s_oauth_authorization: "Приложение OAuth не авторизовано для выполнения этого запроса" 31 | meeting_not_found: "Встреча не найдена или срок ее действия истек" 32 | contact_system_admin: "План Zoom Webinars необходим для подписки на Zoom этого сайта. Обратитесь к своим администраторам." 33 | dashboard: 34 | problem: 35 | s2s_webinar_subscription: "Плагин Zoom: отсутствует подписка Zoom Webinars. Вам нужно оформить подписку Zoom Webinars, чтобы получить доступ к этой функции" 36 | system_messages: 37 | webinar_reminder: 38 | title: "Предстоящий вебинар" 39 | subject_template: "Вебинар скоро начнется" 40 | text_body_template: | 41 | Здравствуйте! 42 | 43 | Это автоматическое извещение с сайта %{site_name} о том, что скоро начнется вебинар, на который вы зарегистрированы. 44 | 45 | <%{url}> 46 | -------------------------------------------------------------------------------- /config/locales/server.sk.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | sk: 8 | -------------------------------------------------------------------------------- /config/locales/server.sl.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | sl: 8 | -------------------------------------------------------------------------------- /config/locales/server.sq.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | sq: 8 | -------------------------------------------------------------------------------- /config/locales/server.sr.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | sr: 8 | -------------------------------------------------------------------------------- /config/locales/server.sv.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | sv: 8 | -------------------------------------------------------------------------------- /config/locales/server.sw.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | sw: 8 | -------------------------------------------------------------------------------- /config/locales/server.te.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | te: 8 | -------------------------------------------------------------------------------- /config/locales/server.th.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | th: 8 | -------------------------------------------------------------------------------- /config/locales/server.tr_TR.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | tr_TR: 8 | site_settings: 9 | zoom_sdk_key: "SDK anahtarı veya İstemci Kimliği (Uygulama Giriş Bilgileri sekmesi altında)" 10 | zoom_sdk_secret: "SDK sırrı veya İstemci sırrı (Uygulama Giriş Bilgileri sekmesi altında)" 11 | zoom_s2s_account_id: "Zoom Server to Server oAuth uygulamanız için Hesap Kimliği (Uygulama Giriş Bilgileri sekmesi altında)." 12 | zoom_s2s_client_id: "Zoom Server to Server oAuth uygulamanız için İstemci Kimliği (Giriş Bilgileri sekmesi altında)." 13 | zoom_s2s_client_secret: "Zoom Server to Server oAuth uygulamanız için İstemci Sırrı (Giriş Bilgileri sekmesi altında)." 14 | s2s_oauth_token: "Server to Server OAuth belirteci. Bu bilgi otomatik olarak doldurulur" 15 | zoom_webhooks_secret_token: "Zoom Server to Server oAuth uygulamanızın Etkinlik Abonelikleri için gizli belirteç (Özellik sekmesi altında). Zoom kullanıcı arayüzünde değişiklik yapıldığında etkinlik meta verilerini güncellemek için kullanılır." 16 | zoom_host_title_override: "Toplantı sahibinin kullanıcı adının altında kullanıcı başlığı yerine değerini görüntülemek için özel bir kullanıcı alanı adı girin." 17 | zoom_send_user_id: "Bir web seminerine katılırken kullanıcının dahili Discourse Kimliğini Zoom'a gönderin. (isteğe bağlı)" 18 | zoom_display_attendees: "Etkinlik katılımcılarının listesini görüntüleyin." 19 | zoom_join_x_mins_before_start: "Web seminerinin başlamasından x dakika önce kullanıcı arayüzündeki Katıl düğmesine geçin. (0, başlangıç saatinden bağımsız olarak etkinlik başlar başlamaz düğmenin değiştirileceği anlamına gelir)" 20 | zoom_enable_sdk_fallback: "Etkinliğe katılma başarısız olursa (örneğin, Zoom'un Web SDK hizmeti çalışmıyorsa) bir Zoom bağlantısı gösterin." 21 | zoom_use_join_url: "Şimdi Katıl düğmesinin, kullanıcıları Zoom tarafından sağlanan katılma url'sine götürmesini sağlayın." 22 | discourse_zoom_plugin_verbose_logging: "Zoom eklentisi için ayrıntılı günlük kaydını etkinleştirin" 23 | errors: 24 | models: 25 | webinar: 26 | attributes: 27 | zoom_id: 28 | webinar_in_use: "Başka bir konu zaten bu web semineriyle ilişkilendirilmiş." 29 | zoom_plugin_errors: 30 | s2s_oauth_authorization: "OAuth uygulaması bu talebi göndermek için yetkili değil" 31 | meeting_not_found: "Toplantı bulunamadı veya süresi doldu" 32 | contact_system_admin: "Bu sitenin Zoom aboneliği için Webinar planı gerekli. Lütfen yöneticilerinizle iletişime geçin." 33 | dashboard: 34 | problem: 35 | s2s_webinar_subscription: "Zoom Eklentisi: Webinar planı eksik. Bu özelliğe erişmek için webinar planına abone olmalısınız" 36 | system_messages: 37 | webinar_reminder: 38 | title: "Yaklaşan Web Semineri" 39 | subject_template: "Bir web semineri başlamak üzere" 40 | text_body_template: | 41 | Merhaba, 42 | 43 | Bu, %{site_name} kayıtlı olduğunuz bir web seminerinin başlamak üzere olduğunu bildirmek için gönderilen otomatik bir mesajdır. 44 | 45 | <%{url} 46 | -------------------------------------------------------------------------------- /config/locales/server.ug.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | ug: 8 | -------------------------------------------------------------------------------- /config/locales/server.uk.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | uk: 8 | -------------------------------------------------------------------------------- /config/locales/server.ur.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | ur: 8 | -------------------------------------------------------------------------------- /config/locales/server.vi.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | vi: 8 | -------------------------------------------------------------------------------- /config/locales/server.zh_CN.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | zh_CN: 8 | site_settings: 9 | zoom_sdk_key: "SDK 密钥或客户端 ID(在“应用凭据”标签页下)" 10 | zoom_sdk_secret: "SDK 密钥或客户端密钥(在“应用凭据”标签页下)" 11 | zoom_s2s_account_id: "Zoom 服务器到服务器 oAuth 应用的帐户 ID(在“应用凭据”标签页下)。" 12 | zoom_s2s_client_id: "Zoom 服务器到服务器 oAuth 应用的客户端 ID(在“凭据”标签页下)。" 13 | zoom_s2s_client_secret: "Zoom 服务器到服务器 oAuth 应用的客户端密钥(在“凭据”标签页下)。" 14 | s2s_oauth_token: "服务器到服务器 OAuth 令牌。将自动填写此信息" 15 | zoom_webhooks_secret_token: "您的 Zoom 服务器到服务器 oAuth 应用的活动订阅的密钥令牌(在“功能”标签页下)。用于在 Zoom UI 中进行更改时更新活动元数据。" 16 | zoom_host_title_override: "输入自定义的用户字段名称以显示其值,而不是主持人用户名下方的用户标题。" 17 | zoom_send_user_id: "加入网络研讨会时,将用户的内部 Discourse ID 发送到 Zoom。(可选)" 18 | zoom_display_attendees: "显示活动参与者名单。" 19 | zoom_join_x_mins_before_start: "在网络研讨会开始前 x 分钟切换到 UI 中的“加入”按钮。(0 表示活动一开始即切换按钮,而不考虑开始时间)" 20 | zoom_enable_sdk_fallback: "如果加入活动失败(例如,如果 Zoom 的 Web SDK 服务关闭),则显示指向 Zoom 的链接。" 21 | zoom_use_join_url: "通过“立即加入”按钮让用户打开 Zoom 提供的加入 URL。" 22 | discourse_zoom_plugin_verbose_logging: "为 Zoom 插件启用详细日志记录" 23 | errors: 24 | models: 25 | webinar: 26 | attributes: 27 | zoom_id: 28 | webinar_in_use: "另一个话题已与此网络研讨会相关联。" 29 | zoom_plugin_errors: 30 | s2s_oauth_authorization: "OAuth 应用未获得发出此请求的授权" 31 | meeting_not_found: "会议未找到或已过期" 32 | contact_system_admin: "此网站的 Zoom 订阅需要在线讲座方案。请联系您的管理员。" 33 | dashboard: 34 | problem: 35 | s2s_webinar_subscription: "Zoom 插件:缺少在线讲座方案。您必须订阅在线讲座方案才能使用此功能" 36 | system_messages: 37 | webinar_reminder: 38 | title: "即将举行的网络研讨会" 39 | subject_template: "网络研讨会即将开始" 40 | text_body_template: | 41 | 您好! 42 | 43 | 这是 %{site_name} 自动发送的邮件,旨在通知您,您注册的网络研讨会即将开始。 44 | 45 | <%{url}> 46 | -------------------------------------------------------------------------------- /config/locales/server.zh_TW.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | zh_TW: 8 | -------------------------------------------------------------------------------- /config/settings.yml: -------------------------------------------------------------------------------- 1 | discourse_zoom: 2 | zoom_enabled: 3 | default: false 4 | client: true 5 | zoom_sdk_key: 6 | default: "" 7 | client: false 8 | secret: true 9 | zoom_sdk_secret: 10 | default: "" 11 | client: false 12 | secret: true 13 | zoom_s2s_account_id: 14 | default: "" 15 | client: false 16 | secret: true 17 | zoom_s2s_client_id: 18 | default: "" 19 | client: false 20 | secret: true 21 | zoom_s2s_client_secret: 22 | default: "" 23 | client: false 24 | secret: true 25 | s2s_oauth_token: 26 | default: "" 27 | client: false 28 | secret: true 29 | zoom_webhooks_secret_token: 30 | default: "" 31 | client: true 32 | secret: true 33 | zoom_host_title_override: 34 | default: "" 35 | client: false 36 | zoom_send_user_id: 37 | default: false 38 | zoom_send_reminder_minutes_before_webinar: 39 | default: 0 40 | zoom_display_attendees: 41 | default: false 42 | client: true 43 | zoom_join_x_mins_before_start: 44 | default: 0 45 | client: true 46 | zoom_enable_sdk_fallback: 47 | default: true 48 | client: true 49 | zoom_use_join_url: 50 | default: false 51 | client: true 52 | discourse_zoom_plugin_verbose_logging: 53 | default: false 54 | client: true 55 | -------------------------------------------------------------------------------- /db/migrate/20191216205611_create_webinars.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | class CreateWebinars < ActiveRecord::Migration[6.0] 3 | def change 4 | create_table :webinars do |t| 5 | t.integer :topic_id 6 | t.string :zoom_id 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20191218154551_create_webinar_users.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | class CreateWebinarUsers < ActiveRecord::Migration[6.0] 3 | def change 4 | create_table :webinar_users do |t| 5 | t.integer :user_id 6 | t.integer :webinar_id 7 | t.integer :type 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20191219200040_add_webinar_attributes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | class AddWebinarAttributes < ActiveRecord::Migration[6.0] 3 | def change 4 | change_table :webinars do |t| 5 | t.string :title 6 | t.datetime :starts_at 7 | t.datetime :ends_at 8 | t.integer :duration 9 | t.string :zoom_host_id 10 | t.timestamps null: false 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20200103155524_add_webinar_settings.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | class AddWebinarSettings < ActiveRecord::Migration[6.0] 3 | def change 4 | change_table :webinars do |t| 5 | t.boolean :host_video 6 | t.boolean :panelists_video 7 | t.integer :approval_type, default: 2, null: false 8 | t.boolean :enforce_login 9 | t.integer :registrants_restrict_number, default: 0, null: false 10 | t.boolean :meeting_authentication 11 | t.boolean :on_demand 12 | t.string :join_url 13 | t.string :password 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /db/migrate/20200103190818_add_registration_status_to_webinar_users.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | class AddRegistrationStatusToWebinarUsers < ActiveRecord::Migration[6.0] 3 | def change 4 | add_column :webinar_users, :registration_status, :integer, default: 0, null: false 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20200108204521_create_zoom_webhooks.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | class CreateZoomWebhooks < ActiveRecord::Migration[6.0] 3 | def change 4 | create_table :zoom_webinar_webhook_events do |t| 5 | t.string :event 6 | t.text :payload 7 | t.integer :webinar_id 8 | t.bigint :zoom_timestamp 9 | t.timestamps 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20200113155455_add_status_to_webinar.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | class AddStatusToWebinar < ActiveRecord::Migration[6.0] 3 | def change 4 | add_column :webinars, :status, :integer, default: 0, null: false 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20200114171405_drop_webinar_users_registration_status.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | class DropWebinarUsersRegistrationStatus < ActiveRecord::Migration[6.0] 3 | def up 4 | remove_column :webinar_users, :registration_status 5 | end 6 | 7 | def down 8 | raise ActiveRecord::IrreversibleMigration 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20200117165426_add_video_url_to_webinars.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | class AddVideoUrlToWebinars < ActiveRecord::Migration[6.0] 3 | def change 4 | add_column :webinars, :video_url, :text 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20200122200143_add_reminder_sent_at_to_webinars.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | class AddReminderSentAtToWebinars < ActiveRecord::Migration[6.0] 3 | def change 4 | add_column :webinars, :reminders_sent_at, :datetime 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20241009162757_alter_webinar_id_to_bigint.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AlterWebinarIdToBigint < ActiveRecord::Migration[7.1] 4 | def up 5 | change_column :webinar_users, :webinar_id, :bigint 6 | end 7 | 8 | def down 9 | raise ActiveRecord::IrreversibleMigration 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import DiscourseRecommended from "@discourse/lint-configs/eslint"; 2 | 3 | export default [...DiscourseRecommended]; 4 | -------------------------------------------------------------------------------- /lib/client.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Zoom 4 | class Client 5 | API_URL = "https://api.zoom.us/v2/" 6 | 7 | def webinar(webinar_id, raw = false) 8 | response = get("webinars/#{webinar_id}") 9 | return response if raw 10 | 11 | data = response.body 12 | start_datetime = DateTime.parse(data[:start_time]) 13 | 14 | { 15 | id: webinar_id, 16 | title: data[:topic], 17 | starts_at: start_datetime, 18 | duration: data[:duration], 19 | ends_at: start_datetime + data[:duration].minutes, 20 | host_id: data[:host_id], 21 | password: data[:password], 22 | host_video: data[:settings][:host_video], 23 | panelists_video: data[:settings][:panelists_video], 24 | approval_type: data[:settings][:approval_type], 25 | enforce_login: data[:settings][:enforce_login], 26 | registrants_restrict_number: data[:settings][:registrants_restrict_number], 27 | meeting_authentication: data[:settings][:meeting_authentication], 28 | on_demand: data[:settings][:on_demand], 29 | join_url: data[:settings][:join_url], 30 | } 31 | end 32 | 33 | def host(host_id) 34 | response = get("users/#{host_id}") 35 | data = response.body 36 | { 37 | name: "#{data[:first_name]} #{data[:last_name]}", 38 | email: data[:email], 39 | avatar_url: data[:pic_url], 40 | } 41 | end 42 | 43 | def panelists(webinar_id, raw = false) 44 | response = get("webinars/#{webinar_id}/panelists") 45 | return response if raw 46 | 47 | data = response.body 48 | { 49 | panelists: 50 | data[:panelists].map do |s| 51 | { 52 | name: s[:name], 53 | email: s[:email], 54 | avatar_url: User.default_template(s[:name]).gsub("{size}", "25"), 55 | } 56 | end, 57 | panelists_count: data[:total_records], 58 | } 59 | end 60 | 61 | def get(endpoint) 62 | Zoom::OAuthClient.new(API_URL, endpoint).get 63 | end 64 | 65 | def post(endpoint, body) 66 | Zoom::OAuthClient.new(API_URL, endpoint).post(body) 67 | end 68 | 69 | def delete(endpoint) 70 | Zoom::OAuthClient.new(API_URL, endpoint).delete 71 | end 72 | 73 | def jwt_token 74 | payload = { iss: SiteSetting.zoom_sdk_key, exp: Time.now.to_i + 3600 } 75 | 76 | JWT.encode(payload, SiteSetting.zoom_sdk_secret) 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/oauth_client.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module Zoom 3 | class OAuthClient 4 | OAUT_URL = "https://zoom.us/oauth/token" 5 | 6 | def initialize(api_url, end_point, authorization = nil) 7 | @api_url = api_url 8 | @end_point = end_point 9 | @max_tries = 5 10 | @tries = 0 11 | 12 | if authorization 13 | @authorization = authorization 14 | else 15 | @oauth = true 16 | @authorization = 17 | ( 18 | if SiteSetting.s2s_oauth_token.empty? 19 | get_oauth 20 | else 21 | SiteSetting.s2s_oauth_token 22 | end 23 | ) 24 | end 25 | end 26 | 27 | def get 28 | response = send_request(:get) 29 | self.parse_response_body response 30 | end 31 | 32 | def post(body) 33 | send_request(:post, body) 34 | end 35 | 36 | def delete 37 | send_request(:delete) 38 | end 39 | 40 | private 41 | 42 | def send_request(method, body = nil) 43 | response = 44 | Excon.send( 45 | method, 46 | "#{@api_url}#{@end_point}", 47 | headers: { 48 | Authorization: "Bearer #{@authorization}", 49 | "Content-Type": "application/json", 50 | }, 51 | body: body&.to_json, 52 | ) 53 | 54 | if [400, 401].include?(response.status) && @tries < @max_tries 55 | get_oauth 56 | response = send_request(method, body) 57 | elsif [400, 401].include?(response.status) && @tries == @max_tries 58 | self.parse_response_body(response) 59 | # This code 200 is a code sent by Zoom not the request status 60 | if response&.body&.dig(:code) == 200 61 | ProblemCheckTracker["s2s_webinar_subscription"].problem!( 62 | details: { 63 | message: response.body[:message], 64 | }, 65 | ) 66 | 67 | authorization_invalid("contact_system_admin") 68 | end 69 | 70 | authorization_invalid 71 | end 72 | 73 | log("Zoom verbose log:\n API error = #{response.inspect}") if response.status != 200 74 | 75 | if response&.body.present? 76 | result = JSON.parse(response.body) 77 | meeting_not_found if (response.status) == 404 && result["code"] == 3001 78 | log("Zoom verbose log:\n API result = #{result.inspect}") 79 | end 80 | response 81 | end 82 | 83 | def parse_response_body(response) 84 | response.body = JSON.parse(response.body, symbolize_names: true) if response.body.present? 85 | response 86 | end 87 | 88 | private 89 | 90 | def log(message) 91 | Rails.logger.warn(message) if SiteSetting.discourse_zoom_plugin_verbose_logging 92 | end 93 | 94 | def get_oauth 95 | @tries += 1 96 | credentials = "#{SiteSetting.zoom_s2s_client_id}:#{SiteSetting.zoom_s2s_client_secret}" 97 | encoded_credentials = Base64.strict_encode64(credentials) 98 | 99 | body = { grant_type: "account_credentials", account_id: SiteSetting.zoom_s2s_account_id } 100 | body = URI.encode_www_form(body) 101 | 102 | response = 103 | Excon.post( 104 | "#{OAUT_URL}?#{body}", 105 | headers: { 106 | Authorization: "Basic #{encoded_credentials}", 107 | "Content-Type": "application/json", 108 | }, 109 | ) 110 | 111 | response.body = JSON.parse(response.body, symbolize_names: true) if response.body.present? 112 | 113 | if response.status == 200 114 | SiteSetting.s2s_oauth_token = response.body[:access_token] 115 | @authorization = response.body[:access_token] 116 | end 117 | end 118 | 119 | def authorization_invalid(custom_message = nil) 120 | custom_message = "s2s_oauth_authorization" if @oauth && !custom_message 121 | 122 | raise Discourse::InvalidAccess.new( 123 | "zoom_plugin_errors", 124 | SiteSetting.s2s_oauth_token, 125 | custom_message: "zoom_plugin_errors.#{custom_message}", 126 | ) 127 | end 128 | 129 | def meeting_not_found 130 | raise Discourse::NotFound.new( 131 | I18n.t("zoom_plugin_errors.meeting_not_found"), 132 | custom_message: "zoom_plugin_errors.meeting_not_found", 133 | ) 134 | end 135 | end 136 | end 137 | -------------------------------------------------------------------------------- /lib/webinar_creator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Zoom 4 | class WebinarCreator 5 | def initialize(topic_id:, zoom_id:, zoom_start_date: nil, zoom_title: nil, user: nil) 6 | @topic_id = topic_id 7 | @zoom_id = Webinar.sanitize_zoom_id(zoom_id) 8 | @zoom_start_date = zoom_start_date 9 | @zoom_title = zoom_title 10 | @zoom_client = Zoom::Client.new 11 | @current_user = user 12 | end 13 | 14 | def run 15 | nonzoom_webinar = @zoom_start_date.present? 16 | 17 | webinar = Webinar.new 18 | if nonzoom_webinar 19 | webinar.attributes = { 20 | starts_at: @zoom_start_date, 21 | title: @zoom_title, 22 | zoom_id: @zoom_id, 23 | status: 2, # marks past event as ended 24 | } 25 | user = @current_user 26 | else 27 | attributes = @zoom_client.webinar(@zoom_id, true).body 28 | webinar.attributes = webinar.convert_attributes_from_zoom(attributes) 29 | 30 | host_data = @zoom_client.host(attributes[:host_id]) 31 | user = User.find_by_email(host_data[:email]) 32 | end 33 | 34 | webinar.topic_id = @topic_id 35 | webinar.save! 36 | 37 | unless user 38 | user = 39 | User.create!( 40 | email: host_data[:email], 41 | username: UserNameSuggester.suggest(host_data[:email]), 42 | name: User.suggest_name(host_data[:email]), 43 | staged: true, 44 | ) 45 | end 46 | WebinarUser.find_or_create_by(user: user, webinar: webinar, type: :host) 47 | register_panelists(webinar) unless nonzoom_webinar 48 | webinar 49 | end 50 | 51 | private 52 | 53 | def register_panelists(webinar) 54 | @zoom_client.panelists(webinar.zoom_id, true).body[:panelists].each do |attrs| 55 | user = User.with_email(Email.downcase(attrs[:email])).first 56 | if !user 57 | user = 58 | User.create!( 59 | email: attrs[:email], 60 | username: UserNameSuggester.suggest(attrs[:email]), 61 | name: User.suggest_name(attrs[:email]), 62 | staged: true, 63 | ) 64 | end 65 | 66 | existin_records = WebinarUser.where(webinar: webinar, user: user) 67 | if existin_records.any? 68 | existin_records.update_all(type: :panelist) 69 | else 70 | WebinarUser.create!(webinar: webinar, user: user, type: :panelist) 71 | end 72 | end 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/webinars.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Zoom 4 | class Webinars 5 | attr_reader :zoom_client 6 | 7 | RECURRING_WEBINAR_TYPE = 9 8 | 9 | def initialize(zoom_client) 10 | @zoom_client = zoom_client 11 | end 12 | 13 | def unmatched(user) 14 | response = zoom_client.get("users/#{user.email}/webinars?page_size=300") 15 | return [] unless response 16 | 17 | result = 18 | response 19 | &.body 20 | &.[](:webinars) 21 | &.select do |hash| 22 | hash[:start_time].in_time_zone.utc > Time.now.utc && 23 | hash[:type] != RECURRING_WEBINAR_TYPE && Webinar.where(zoom_id: hash[:id]).empty? 24 | end 25 | 26 | result 27 | end 28 | 29 | def find(webinar_id) 30 | webinar_data = zoom_client.webinar(webinar_id) 31 | return false unless webinar_data[:id] 32 | webinar_data[:panelists] = panelists(webinar_id) 33 | webinar_data[:host] = host(webinar_data[:host_id]) 34 | 35 | existing_topic = Webinar.where(zoom_id: webinar_id).first 36 | webinar_data[:existing_topic] = existing_topic if existing_topic.present? 37 | 38 | webinar_data 39 | end 40 | 41 | def add_panelist(webinar:, user:) 42 | response = 43 | zoom_client.post( 44 | "webinars/#{webinar.zoom_id}/panelists", 45 | panelists: [{ email: user.email, name: (user.name.presence || user.username) }], 46 | ) 47 | return false if response.status != 201 48 | 49 | WebinarUser.where(user: user, webinar: webinar).destroy_all 50 | WebinarUser.create!(user: user, webinar: webinar, type: :panelist) 51 | end 52 | 53 | def remove_panelist(webinar:, user:) 54 | panelists = zoom_client.panelists(webinar.zoom_id, true)[:body][:panelists] 55 | matching_panelist = panelists.detect { |panelist| panelist[:email] == user.email } 56 | return false unless matching_panelist 57 | 58 | response = 59 | zoom_client.delete("webinars/#{webinar.zoom_id}/panelists/#{matching_panelist[:id]}") 60 | return false if response.status != 204 61 | 62 | WebinarUser.where(user: user, webinar: webinar).destroy_all 63 | end 64 | 65 | def signature(webinar_id) 66 | return false if !SiteSetting.zoom_sdk_key && !SiteSetting.zoom_sdk_secret 67 | webinar = zoom_client.webinar(webinar_id) 68 | 69 | return false unless webinar[:id] 70 | 71 | iat = DateTime.now.utc - 30.seconds 72 | exp = iat + 2.hours 73 | header = { alg: "HS256", typ: "JWT" } 74 | role = "0" # regular member role 75 | 76 | payload = { 77 | sdkKey: SiteSetting.zoom_sdk_key, 78 | appKey: SiteSetting.zoom_sdk_key, 79 | mn: webinar_id, 80 | role: role, 81 | iat: iat.to_i, 82 | exp: exp.to_i, 83 | tokenExp: exp.to_i, 84 | } 85 | 86 | JWT.encode(payload, SiteSetting.zoom_sdk_secret, "HS256", header) 87 | end 88 | 89 | private 90 | 91 | def host(host_id) 92 | host_data = zoom_client.host(host_id) 93 | user = User.find_by_email(host_data[:email]) 94 | return host_data if user.nil? 95 | 96 | host_payload(user) 97 | end 98 | 99 | def panelists(webinar_id) 100 | panelists_data = zoom_client.panelists(webinar_id) 101 | panelist_emails = panelists_data[:panelists].map { |s| s[:email] }.join(",") 102 | panelists = User.with_email(panelist_emails) 103 | 104 | if panelists.empty? 105 | panelists = 106 | panelists_data[:panelists].map { |s| { name: s[:name], avatar_url: s[:avatar_url] } } 107 | return panelists 108 | end 109 | 110 | panelists_payload(panelists) 111 | end 112 | 113 | def panelists_payload(panelists) 114 | panelists.map do |s| 115 | { name: s.name || s.username, avatar_url: s.avatar_template_url.gsub("{size}", "25") } 116 | end 117 | end 118 | 119 | def host_payload(host) 120 | if SiteSetting.zoom_host_title_override 121 | field_id = UserField.where(name: SiteSetting.zoom_host_title_override).pluck(:id).first 122 | title = host.user_fields[field_id.to_s] || "" 123 | else 124 | title = host.title 125 | end 126 | { 127 | name: host.name || host.username, 128 | title: title, 129 | avatar_url: host.avatar_template_url.gsub("{size}", "120"), 130 | } 131 | end 132 | end 133 | end 134 | -------------------------------------------------------------------------------- /lib/zoom/topic_extension.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Zoom 4 | module TopicExtension 5 | extend ActiveSupport::Concern 6 | 7 | prepended { has_one :webinar } 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/zoom/user_extension.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Zoom 4 | module UserExtension 5 | extend ActiveSupport::Concern 6 | 7 | prepended { has_many :webinar_users } 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "devDependencies": { 4 | "@discourse/lint-configs": "2.32.0", 5 | "ember-template-lint": "7.9.1", 6 | "eslint": "9.37.0", 7 | "prettier": "3.6.2", 8 | "stylelint": "16.25.0" 9 | }, 10 | "engines": { 11 | "node": ">= 22", 12 | "npm": "please-use-pnpm", 13 | "yarn": "please-use-pnpm", 14 | "pnpm": "9.x" 15 | }, 16 | "packageManager": "pnpm@9.15.5" 17 | } 18 | -------------------------------------------------------------------------------- /plugin.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # name: discourse-zoom 4 | # about: Integrates Zoom webinars into Discourse. 5 | # meta_topic_id: 142711 6 | # version: 0.0.1 7 | # authors: Penar Musaraj, Roman Rizzi, Mark VanLandingham 8 | # url: https://github.com/discourse/discourse-zoom 9 | 10 | enabled_site_setting :zoom_enabled 11 | register_asset "stylesheets/common/zoom.scss" 12 | register_asset "stylesheets/common/webinar-picker.scss" 13 | register_asset "stylesheets/common/webinar-details.scss" 14 | 15 | register_svg_icon "far-circle-check" 16 | register_svg_icon "far-calendar-days" 17 | register_svg_icon "video" 18 | 19 | after_initialize do 20 | require_relative "app/services/problem_check/s2s_webinar_subscription.rb" 21 | register_problem_check ProblemCheck::S2sWebinarSubscription 22 | module ::Zoom 23 | PLUGIN_NAME = "discourse-zoom".freeze 24 | 25 | class Engine < ::Rails::Engine 26 | engine_name Zoom::PLUGIN_NAME 27 | isolate_namespace Zoom 28 | end 29 | end 30 | 31 | require_relative "app/models/webinar" 32 | require_relative "app/models/webinar_user" 33 | require_relative "app/models/zoom_webinar_webhook_event" 34 | require_relative "lib/webinars" 35 | require_relative "lib/client" 36 | require_relative "lib/oauth_client" 37 | require_relative "lib/webinar_creator" 38 | require_relative "app/controllers/webinars_controller" 39 | require_relative "app/controllers/webhooks_controller" 40 | require_relative "app/serializers/host_serializer" 41 | require_relative "app/serializers/webinar_serializer" 42 | require_relative "app/jobs/scheduled/send_webinar_reminders" 43 | require_relative "lib/zoom/user_extension" 44 | require_relative "lib/zoom/topic_extension" 45 | 46 | reloadable_patch do |plugin| 47 | User.prepend(Zoom::UserExtension) 48 | Topic.prepend(Zoom::TopicExtension) 49 | end 50 | 51 | add_to_serializer(:topic_view, :webinar) do 52 | WebinarSerializer.new(object.topic.webinar, root: false).as_json 53 | end 54 | 55 | add_to_serializer(:current_user, :webinar_registrations) do 56 | object.webinar_users.as_json(only: %i[user_id type webinar_id]) 57 | end 58 | 59 | add_permitted_post_create_param(:zoom_id) 60 | add_permitted_post_create_param(:zoom_webinar_title) 61 | add_permitted_post_create_param(:zoom_webinar_start_date) 62 | 63 | on(:post_created) do |post, opts, user| 64 | if opts[:zoom_id] && post.is_first_post? 65 | zoom_start_date = opts[:zoom_webinar_start_date] 66 | zoom_title = opts[:zoom_webinar_title] 67 | 68 | Zoom::WebinarCreator.new( 69 | topic_id: post.topic_id, 70 | zoom_id: opts[:zoom_id], 71 | zoom_start_date: zoom_start_date, 72 | zoom_title: zoom_title, 73 | user: user, 74 | ).run 75 | end 76 | end 77 | 78 | Zoom::Engine.routes.draw do 79 | resources :webinars, only: %i[show index destroy] do 80 | put "attendees/:username" => "webinars#register", 81 | :constraints => { 82 | username: RouteFormat.username, 83 | format: :json, 84 | } 85 | put "attendees/:username/watch" => "webinars#watch", 86 | :constraints => { 87 | username: RouteFormat.username, 88 | format: :json, 89 | } 90 | delete "attendees/:username" => "webinars#unregister", 91 | :constraints => { 92 | username: RouteFormat.username, 93 | format: :json, 94 | } 95 | put "panelists/:username" => "webinars#add_panelist", 96 | :constraints => { 97 | username: RouteFormat.username, 98 | format: :json, 99 | } 100 | delete "panelists/:username" => "webinars#remove_panelist", 101 | :constraints => { 102 | username: RouteFormat.username, 103 | format: :json, 104 | } 105 | put "nonzoom_host/:username" => "webinars#update_nonzoom_host", 106 | :constraints => { 107 | username: RouteFormat.username, 108 | format: :json, 109 | } 110 | delete "nonzoom_host/:username" => "webinars#delete_nonzoom_host", 111 | :constraints => { 112 | username: RouteFormat.username, 113 | format: :json, 114 | } 115 | put "nonzoom_details" => "webinars#update_nonzoom_details", :constraints => { format: :json } 116 | put "video_url" => "webinars#set_video_url" 117 | get "preview" => "webinars#preview" 118 | get "sdk" => "webinars#sdk" 119 | get "signature" => "webinars#signature" 120 | end 121 | put "t/:topic_id/webinars/:zoom_id" => "webinars#add_to_topic" 122 | 123 | post "/webhooks/webinars" => "webhooks#webinars" 124 | end 125 | 126 | Discourse::Application.routes.append do 127 | mount ::Zoom::Engine, at: "/zoom" 128 | get "topics/webinar-registrations/:username" => "list#zoom_webinars", 129 | :as => "topics_zoom_webinars", 130 | :constraints => { 131 | username: ::RouteFormat.username, 132 | } 133 | end 134 | 135 | ListController.generate_message_route(:zoom_webinars) 136 | 137 | add_to_class(:topic_query, :list_zoom_webinars) do |user| 138 | list = 139 | joined_topic_user 140 | .joins(webinar: :webinar_users) 141 | .where("webinar_users.user_id = ?", user.id.to_s) 142 | .order("webinars.starts_at DESC") 143 | 144 | create_list(:webinars, {}, list) 145 | end 146 | 147 | extend_content_security_policy(script_src: ["'unsafe-eval'"]) 148 | 149 | ::ActionController::Base.prepend_view_path File.expand_path("../app/views", __FILE__) 150 | end 151 | -------------------------------------------------------------------------------- /public/javascripts/webinar-join.js: -------------------------------------------------------------------------------- 1 | window.onload = (event) => { 2 | (function () { 3 | document.querySelector(".d-header").style.display = "none"; 4 | 5 | ZoomMtg.preLoadWasm(); 6 | ZoomMtg.prepareWebSDK(); 7 | 8 | const path = window.location.pathname; 9 | const meetingId = path.split("/zoom/webinars/")[1].split("/sdk")[0]; 10 | let getParams = function (url) { 11 | let params = {}; 12 | let parser = document.createElement("a"); 13 | parser.href = url; 14 | let query = parser.search.substring(1); 15 | let vars = query.split("&"); 16 | for (let i = 0; i < vars.length; i++) { 17 | let pair = vars[i].split("="); 18 | params[pair[0]] = decodeURIComponent(pair[1]); 19 | } 20 | return params; 21 | }; 22 | 23 | let request = new XMLHttpRequest(); 24 | request.open("GET", `/zoom/webinars/${meetingId}/signature.json`, true); 25 | 26 | request.onload = function () { 27 | if (this.status >= 200 && this.status < 400) { 28 | let res = JSON.parse(this.response); 29 | console.log(res); 30 | ZoomMtg.init({ 31 | leaveUrl: res.topic_url, 32 | isSupportAV: true, 33 | patchJsMedia: true, 34 | // audioPanelAlwaysOpen: false, 35 | // disableJoinAudio: true, 36 | disableCallOut: true, 37 | success: function () { 38 | ZoomMtg.join({ 39 | signature: res.signature, 40 | sdkKey: res.sdk_key, 41 | meetingNumber: res.id, 42 | passWord: res.password || "", 43 | userName: res.username, 44 | userEmail: res.email, 45 | success: function (res) {}, 46 | error: function (join_result) { 47 | console.log(join_result); 48 | if (join_result.errorCode === 1) { 49 | const params = getParams(window.location.href); 50 | if (params.fallback) { 51 | window.setTimeout(() => { 52 | let btn = `Launch in Zoom`; 53 | document.querySelector( 54 | ".zm-modal-body-content .content" 55 | ).innerHTML = 56 | `

There was a problem launching the Zoom SDK. Click the button below to try joining the event in Zoom.

${btn}`; 57 | }, 200); 58 | } 59 | } 60 | }, 61 | }); 62 | }, 63 | error: function (res) { 64 | console.log("error generating signature"); 65 | }, 66 | }); 67 | } else { 68 | console.error( 69 | "error getting webinar signature from discourse-zoom plugin" 70 | ); 71 | } 72 | }; 73 | 74 | // request.onerror = function() {}; 75 | request.send(); 76 | })(); 77 | }; 78 | -------------------------------------------------------------------------------- /spec/fabricators/webinar_fabricator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails_helper" 4 | 5 | Fabricator(:webinar) do 6 | title "Test webinar" 7 | zoom_id { sequence(:zoom_id) } 8 | starts_at 6.hours.from_now 9 | ends_at 7.hours.from_now 10 | duration 60 11 | zoom_host_id "a1a1k1k30291" 12 | end 13 | -------------------------------------------------------------------------------- /spec/jobs/remind_webinar_attendees_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require_relative "../fabricators/webinar_fabricator.rb" 3 | 4 | RSpec.describe Zoom::SendWebinarReminders do 5 | fab!(:topic_1, :topic) 6 | fab!(:topic_2, :topic) 7 | fab!(:user_1, :user) 8 | fab!(:user_2, :user) 9 | let(:needs_reminding) do 10 | Fabricate(:webinar, topic: topic_1, starts_at: DateTime.now + 20.minutes) 11 | end 12 | let(:no_reminder) { Fabricate(:webinar, topic: topic_2, starts_at: DateTime.now + 2.days) } 13 | 14 | before do 15 | Jobs.run_immediately! 16 | needs_reminding.webinar_users.create(user: user_1, type: :attendee) 17 | needs_reminding.webinar_users.create(user: user_2, type: :attendee) 18 | no_reminder.webinar_users.create(user: user_1, type: :attendee) 19 | end 20 | describe "with zoom_send_reminder_minutes_before_webinar set to 0" do 21 | before { SiteSetting.zoom_send_reminder_minutes_before_webinar = "" } 22 | 23 | it "does not send reminders" do 24 | SystemMessage 25 | .any_instance 26 | .expects(:create) 27 | .with() { |value| value == "webinar_reminder" } 28 | .never 29 | Zoom::SendWebinarReminders.new.execute({}) 30 | end 31 | end 32 | 33 | describe "with zoom_send_reminder_minutes_before_webinar set to 0" do 34 | before { SiteSetting.zoom_send_reminder_minutes_before_webinar = "0" } 35 | 36 | it "does not send reminders" do 37 | SystemMessage 38 | .any_instance 39 | .expects(:create) 40 | .with() { |value| value == "webinar_reminder" } 41 | .never 42 | Zoom::SendWebinarReminders.new.execute({}) 43 | end 44 | end 45 | 46 | describe "with zoom_send_reminder_minutes_before_webinar set to 30 minutes" do 47 | before { SiteSetting.zoom_send_reminder_minutes_before_webinar = "30" } 48 | it "sends reminders for upcoming webinars" do 49 | expect(needs_reminding.reload.reminders_sent_at).to eq(nil) 50 | SystemMessage 51 | .any_instance 52 | .expects(:create) 53 | .with() { |value| value == "webinar_reminder" } 54 | .twice 55 | 56 | Zoom::SendWebinarReminders.new.execute({}) 57 | expect(needs_reminding.reload.reminders_sent_at).not_to eq(nil) 58 | end 59 | 60 | it "does not re-send to those already reminded" do 61 | needs_reminding.update(reminders_sent_at: DateTime.now) 62 | 63 | SystemMessage 64 | .any_instance 65 | .expects(:create) 66 | .with() { |value| value == "webinar_reminder" } 67 | .never 68 | Zoom::SendWebinarReminders.new.execute({}) 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /spec/lib/post_creator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails_helper" 4 | require_relative "../responses/zoom_api_stubs" 5 | 6 | describe PostCreator do 7 | let!(:title) { "Testing Zoom Webinar integration" } 8 | let(:zoom_id) { "123" } 9 | let(:user) { Fabricate(:user, refresh_auto_groups: true) } 10 | 11 | before do 12 | SiteSetting.fast_typing_threshold = "disabled" 13 | SiteSetting.zoom_enabled = true 14 | SiteSetting.s2s_oauth_token = "Test_Token" 15 | end 16 | 17 | describe "creating a topic with webinar" do 18 | it "works" do 19 | stub_request(:get, "https://api.zoom.us/v2/webinars/#{zoom_id}").to_return( 20 | status: 201, 21 | body: ZoomApiStubs.get_webinar(zoom_id), 22 | ) 23 | stub_request(:get, "https://api.zoom.us/v2/users/#{zoom_id}").to_return( 24 | status: 201, 25 | body: ZoomApiStubs.get_host(zoom_id), 26 | ) 27 | stub_request(:get, "https://api.zoom.us/v2/webinars/#{zoom_id}/panelists").to_return( 28 | status: 201, 29 | body: { panelists: [{ id: "123", email: user.email }] }.to_json, 30 | ) 31 | 32 | post = 33 | PostCreator.new( 34 | user, 35 | title: title, 36 | raw: "Here comes the rain again", 37 | zoom_id: zoom_id, 38 | ).create 39 | 40 | expect(post.topic.webinar.zoom_id).to eq(zoom_id) 41 | end 42 | 43 | it "creates a past webinar without calling Zoom API" do 44 | post = 45 | PostCreator.new( 46 | user, 47 | title: title, 48 | raw: "Falling on my head like a new emotion", 49 | zoom_id: "nonzoom", 50 | zoom_webinar_start_date: "Mon Mar 02 2020 00:00:00 GMT-0500 (Eastern Standard Time)", 51 | zoom_webinar_title: "This is a non-Zoom webinar", 52 | ).create 53 | 54 | expect(post.topic.webinar.zoom_id).to eq("nonzoom") 55 | expect(post.topic.webinar.host.username).to eq(user.username) 56 | expect(post.topic.webinar.title).to eq("This is a non-Zoom webinar") 57 | end 58 | 59 | it "ignores webinar params in replies" do 60 | topic = Fabricate(:topic) 61 | Fabricate(:post, topic: topic) 62 | 63 | post = 64 | PostCreator.new( 65 | user, 66 | title: title, 67 | raw: "You know what they say...", 68 | zoom_id: "nonzoom", 69 | zoom_webinar_start_date: "Mon Mar 02 2020 00:00:00 GMT-0500 (Eastern Standard Time)", 70 | zoom_webinar_title: "This is a non-Zoom webinar", 71 | topic_id: topic.id, 72 | ).create 73 | 74 | expect(post.topic.webinar).to eq(nil) 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /spec/lib/webinar_creator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Zoom::WebinarCreator do 4 | describe "#run" do 5 | it "creates a webinar" do 6 | stub_request( 7 | :post, 8 | "https://zoom.us/oauth/token?account_id=&grant_type=account_credentials", 9 | ).to_return(status: 200, body: {}.to_json, headers: {}) 10 | stub_request(:get, "https://api.zoom.us/v2/webinars/123").to_return( 11 | status: 201, 12 | body: ZoomApiStubs.get_webinar(123, 456), 13 | ) 14 | stub_request(:get, "https://api.zoom.us/v2/users/456").to_return( 15 | status: 200, 16 | body: ZoomApiStubs.get_host("456"), 17 | ) 18 | stub_request(:get, "https://api.zoom.us/v2/webinars/123/panelists").to_return( 19 | status: 201, 20 | body: { panelists: [] }.to_json, 21 | ) 22 | 23 | creator = described_class.new(topic_id: 12_112, zoom_id: "123") 24 | 25 | expect { creator.run }.to change { Webinar.count }.by(1) 26 | expect(Webinar.last).to have_attributes(zoom_id: "123", zoom_host_id: "456") 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/lib/zoom_client_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Zoom::Client do 4 | describe "#webinar" do 5 | it "returns a webinar object" do 6 | stub_request( 7 | :post, 8 | "https://zoom.us/oauth/token?account_id=&grant_type=account_credentials", 9 | ).to_return(status: 200, body: {}.to_json, headers: {}) 10 | stub_request(:get, "https://api.zoom.us/v2/webinars/123").to_return( 11 | status: 201, 12 | body: ZoomApiStubs.get_webinar(123), 13 | ) 14 | 15 | client = described_class.new 16 | webinar_response = JSON.parse(ZoomApiStubs.get_webinar(123)) 17 | webinar = client.webinar("123") 18 | 19 | expect(webinar).to eq( 20 | id: "#{webinar_response["id"]}", 21 | title: webinar_response["topic"], 22 | starts_at: DateTime.parse(webinar_response["start_time"]), 23 | duration: webinar_response["duration"], 24 | ends_at: 25 | DateTime.parse(webinar_response["start_time"]) + 26 | webinar_response["duration"].to_i.minutes, 27 | host_id: webinar_response["host_id"], 28 | password: webinar_response["password"], 29 | host_video: webinar_response["settings"]["host_video"], 30 | panelists_video: webinar_response["settings"]["panelists_video"], 31 | approval_type: webinar_response["settings"]["approval_type"], 32 | enforce_login: webinar_response["settings"]["enforce_login"], 33 | registrants_restrict_number: webinar_response["settings"]["registrants_restrict_number"], 34 | meeting_authentication: webinar_response["settings"]["meeting_authentication"], 35 | on_demand: webinar_response["settings"]["on_demand"], 36 | join_url: webinar_response["settings"]["join_url"], 37 | ) 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/models/webinar_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails_helper" 4 | 5 | describe Webinar do 6 | fab!(:first_user, :user) 7 | fab!(:second_user, :user) 8 | fab!(:third_user, :user) 9 | fab!(:fourth_user, :user) 10 | fab!(:topic) { Fabricate(:topic, user: first_user) } 11 | let(:webinar) { Webinar.create(topic: topic, zoom_id: "123") } 12 | 13 | describe "unique zoom_id" do 14 | it "does not create a duplicate" do 15 | webinar.save 16 | webinar_dupe = Webinar.create(topic: topic, zoom_id: "123") 17 | expect(webinar_dupe.save).to eq(false) 18 | end 19 | 20 | it "does not validate uniqueness on nonzoom events" do 21 | webinarA = Webinar.create(topic: topic, zoom_id: "nonzoom") 22 | webinarA.save 23 | webinarB = Webinar.create(topic: topic, zoom_id: "nonzoom") 24 | expect(webinarB.save).to eq(true) 25 | end 26 | end 27 | 28 | describe ".sanitize_zoom_id" do 29 | it "removes spaces and dashes" do 30 | id = " 342-265-6531" 31 | expect(Webinar.sanitize_zoom_id(id)).to eq("3422656531") 32 | end 33 | end 34 | 35 | describe "users" do 36 | before do 37 | webinar.webinar_users.create(user: first_user, type: :attendee) 38 | webinar.webinar_users.create(user: second_user, type: :panelist) 39 | webinar.webinar_users.create(user: third_user, type: :host) 40 | webinar.webinar_users.create(user: fourth_user, type: :attendee) 41 | end 42 | describe "#attendees" do 43 | it "gets attendees" do 44 | attendees = webinar.attendees 45 | expect(attendees.count).to eq(2) 46 | expect(attendees.include? first_user).to eq(true) 47 | expect(attendees.include? fourth_user).to eq(true) 48 | end 49 | end 50 | describe "#panelists" do 51 | it "gets panelists" do 52 | panelists = webinar.panelists 53 | expect(panelists.count).to eq(1) 54 | expect(panelists.first).to eq(second_user) 55 | end 56 | end 57 | describe "#host" do 58 | it "gets host" do 59 | expect(webinar.host).to eq(third_user) 60 | end 61 | end 62 | end 63 | 64 | describe "update" do 65 | it "publishes to message_bus if status changed" do 66 | MessageBus.expects(:publish).with("/zoom/webinars/#{webinar.id}", status: "started") 67 | webinar.update(status: :started) 68 | end 69 | 70 | it "does not publishes to message_bus if status is unchanged" do 71 | MessageBus.expects(:publish).never 72 | webinar.update(duration: 120) 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /spec/plugin_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "responses/zoom_api_stubs" 4 | 5 | RSpec.configure { |config| config.include ZoomApiStubs } 6 | -------------------------------------------------------------------------------- /spec/requests/oauth_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails_helper" 4 | require_relative "../responses/zoom_api_stubs" 5 | 6 | describe Zoom::OAuthClient do 7 | fab!(:user) 8 | fab!(:admin) { Fabricate(:user, username: "admin.user", admin: true) } 9 | fab!(:topic) { Fabricate(:topic, user: user) } 10 | let!(:webinar) { Webinar.create(topic: topic, zoom_id: "123") } 11 | let!(:valid_token) { "valid_token" } 12 | let!(:invalid_token) { "invalid_token" } 13 | let!(:end_point) { "webinars/#{webinar.zoom_id}" } 14 | let!(:body) do 15 | body = { grant_type: "account_credentials", account_id: SiteSetting.zoom_s2s_account_id } 16 | body = URI.encode_www_form(body) 17 | body 18 | end 19 | before { SiteSetting.zoom_enabled = true } 20 | 21 | describe "#get_oauth" do 22 | describe "oauth_token" do 23 | before do 24 | SiteSetting.zoom_s2s_account_id = "123456" 25 | stub_request(:get, "#{Zoom::Client::API_URL}#{end_point}").with( 26 | headers: { 27 | Authorization: "Bearer #{valid_token}", 28 | Host: "api.zoom.us", 29 | }, 30 | ).to_return(body: ZoomApiStubs.get_webinar(user.id), status: 200) 31 | stub_request(:get, "#{Zoom::Client::API_URL}#{end_point}").with( 32 | headers: { 33 | Authorization: "Bearer #{invalid_token}", 34 | Host: "api.zoom.us", 35 | }, 36 | ).to_return(body: "", status: 400) 37 | stub_request(:get, "#{Zoom::Client::API_URL}#{end_point}").with( 38 | headers: { 39 | Authorization: "Bearer not_a_valid_token", 40 | Host: "api.zoom.us", 41 | }, 42 | ).to_return(body: ZoomApiStubs.get_webinar(user.id), status: 400) 43 | end 44 | describe "valid/present" do 45 | before { SiteSetting.s2s_oauth_token = valid_token } 46 | it "makes api calls" do 47 | response = described_class.new(Zoom::Client::API_URL, end_point).get 48 | expect(response.status).to eq(200) 49 | end 50 | end 51 | describe "invalid/not present" do 52 | describe "uses valid account authorization" do 53 | before do 54 | stub_request( 55 | :post, 56 | "https://zoom.us/oauth/token?account_id=123456&grant_type=account_credentials", 57 | ).with( 58 | headers: { 59 | Authorization: "Basic Og==", 60 | Content_Type: "application/json", 61 | Host: "zoom.us", 62 | }, 63 | ).to_return( 64 | body: { access_token: valid_token }.to_json, 65 | headers: { 66 | content_type: "application/json", 67 | }, 68 | status: 200, 69 | ) 70 | end 71 | it "requests a new oauth_token" do 72 | SiteSetting.s2s_oauth_token = invalid_token 73 | response = described_class.new(Zoom::Client::API_URL, end_point).get 74 | expect(response.status).to eq(200) 75 | end 76 | end 77 | 78 | describe "uses invalid authorization" do 79 | before do 80 | stub_request( 81 | :post, 82 | "https://zoom.us/oauth/token?account_id=123456&grant_type=account_credentials", 83 | ).with( 84 | headers: { 85 | Authorization: "Basic Og==", 86 | Content_Type: "application/json", 87 | Host: "zoom.us", 88 | }, 89 | ).to_return( 90 | body: { access_token: "not_a_valid_token" }.to_json, 91 | headers: { 92 | content_type: "application/json", 93 | }, 94 | status: 400, 95 | ) 96 | end 97 | it "can't request a new oauth_token" do 98 | SiteSetting.s2s_oauth_token = "not_a_valid_token" 99 | expect { described_class.new(Zoom::Client::API_URL, end_point).get }.to raise_error( 100 | Discourse::InvalidAccess, 101 | ) 102 | end 103 | end 104 | end 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /spec/responses/zoom_api_stubs.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | module ZoomApiStubs 3 | def self.get_webinar(id, host_id = 123) 4 | { 5 | agenda: "", 6 | created_at: "2020-01-06T17:14:44Z", 7 | duration: 120, 8 | host_id: host_id, 9 | id: id, 10 | join_url: "https://zoom.us/j/243200?", 11 | password: "828943", 12 | settings: { 13 | allow_multiple_devices: true, 14 | alternative_hosts: "", 15 | approval_type: 2, 16 | audio: "telephony", 17 | authentication_domains: "", 18 | authentication_option: "signin_tzr4QYF8ng", 19 | auto_recording: "none", 20 | close_registration: true, 21 | contact_email: "mark.vanlandingham@discourse.org", 22 | contact_name: "Mark VanLandingham", 23 | enforce_login: true, 24 | enforce_login_domains: "", 25 | global_dial_in_countries: ["US"], 26 | global_dial_in_numbers: [ 27 | { 28 | city: "New York", 29 | country: "US", 30 | country_name: "US", 31 | number: "+1 9292056099", 32 | type: "toll", 33 | }, 34 | { 35 | city: "San Jose", 36 | country: "US", 37 | country_name: "US", 38 | number: "+1 6699006833", 39 | type: "toll", 40 | }, 41 | ], 42 | hd_video: false, 43 | host_video: false, 44 | meeting_authentication: true, 45 | on_demand: false, 46 | panelists_video: true, 47 | post_webinar_survey: false, 48 | practice_session: false, 49 | question_answer: true, 50 | registrants_confirmation_email: true, 51 | registrants_email_notification: true, 52 | registrants_restrict_number: 0, 53 | registration_type: 1, 54 | show_share_button: true, 55 | }, 56 | start_time: "2020-02-29T18:00:00Z", 57 | start_url: "https://zoomVobWJRMiZXhw.6rbaPZihxtSahkBge9lcRcAsAV8T34ZvEfy2IymiZRo", 58 | timezone: "America/Los_Angeles", 59 | topic: "Mark's test #2", 60 | type: 5, 61 | uuid: "6YdIxCiqSHy3+gs06iPesw==", 62 | }.to_json 63 | end 64 | 65 | def self.get_host(host_id) 66 | { 67 | account_id: "scvbcvbcvbcvbcvbcvsdhgbsdf", 68 | created_at: "2019-12-16T16:43:31Z", 69 | dept: "", 70 | email: "mark.vanlandingham@discourse.org", 71 | first_name: "Mark", 72 | group_ids: ["ncvbm,cvxnb"], 73 | host_key: "dkfjnbvdxfkjbvvdsf", 74 | id: "dkfhjvbsdk,fv", 75 | im_group_ids: [], 76 | jid: "bwxa@xmpp.zoom.us", 77 | job_title: "", 78 | language: "", 79 | last_login_time: "2020-01-16T20:23:28Z", 80 | last_name: "VanLandingham", 81 | location: "", 82 | personal_meeting_url: "https://zoasgd", 83 | phone_country: "", 84 | phone_number: "", 85 | pic_url: "https://lh3.googfg.com", 86 | pmi: 480, 87 | role_name: "Developer", 88 | status: "active", 89 | timezone: "", 90 | type: 2, 91 | use_pmi: false, 92 | verified: 0, 93 | }.to_json 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /spec/system/core_features_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "Core features", type: :system do 4 | before { enable_current_plugin } 5 | 6 | it_behaves_like "having working core features" 7 | end 8 | -------------------------------------------------------------------------------- /spec/system/topic_page_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe "Discourse Zoom | Topic Page", type: :system do 4 | %w[enabled disabled].each do |value| 5 | before { SiteSetting.glimmer_post_stream_mode = value } 6 | 7 | context "when glimmer_post_stream_mode=#{value}" do 8 | fab!(:topic, :topic_with_op) 9 | fab!(:webinar) { Webinar.create(topic:, zoom_id: "123") } 10 | 11 | before { SiteSetting.zoom_enabled = true } 12 | 13 | it "renders successfully" do 14 | visit "/t/#{topic.slug}/#{topic.id}" 15 | expect(page).to have_css(".webinar-banner") 16 | expect(page).to have_css("body.has-webinar") 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /stylelint.config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | extends: ["@discourse/lint-configs/stylelint"], 3 | }; 4 | -------------------------------------------------------------------------------- /test/javascripts/integration/components/webinar-register-test.gjs: -------------------------------------------------------------------------------- 1 | import { render } from "@ember/test-helpers"; 2 | import { module, test } from "qunit"; 3 | import { setupRenderingTest } from "discourse/tests/helpers/component-test"; 4 | import WebinarRegister from "discourse/plugins/discourse-zoom/discourse/components/webinar-register"; 5 | 6 | module("Integration | Component | webinar-register", function (hooks) { 7 | setupRenderingTest(hooks); 8 | 9 | test("Google Calendar link", async function (assert) { 10 | const self = this; 11 | 12 | const webinar = { 13 | id: 99, 14 | title: "Spider Webinar", 15 | status: "scheduled", 16 | ends_at: "2031-09-01T12:00:00Z", 17 | starts_at: "2031-09-01T11:00:00Z", 18 | attendees: [{ id: 1 }], 19 | panelists: [{ id: 1 }], 20 | host: { id: 101 }, 21 | join_url: "https://zoom.us/j/123456789", 22 | }; 23 | this.set("webinar", webinar); 24 | this.currentUser.id = 101; 25 | 26 | await render( 27 | 33 | ); 34 | assert 35 | .dom(".zoom-add-to-calendar-container a") 36 | .hasAttribute( 37 | "href", 38 | "http://www.google.com/calendar/event?action=TEMPLATE&text=Spider%20Webinar&dates=20310901T110000Z/20310901T120000Z&details=Join%20from%20a%20PC%2C%20Mac%2C%20iPad%2C%20iPhone%20or%20Android%20device%3A%0A%20%20%20%20Please%20click%20this%20URL%20to%20join.%20%3Ca%20href%3D%22https%3A%2F%2Fzoom.us%2Fj%2F123456789%22%3Ehttps%3A%2F%2Fzoom.us%2Fj%2F123456789%3C%2Fa%3E&location=https%3A%2F%2Fzoom.us%2Fj%2F123456789" 39 | ); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /translator.yml: -------------------------------------------------------------------------------- 1 | # Configuration file for discourse-translator-bot 2 | 3 | files: 4 | - source_path: config/locales/client.en.yml 5 | destination_path: client.yml 6 | - source_path: config/locales/server.en.yml 7 | destination_path: server.yml 8 | --------------------------------------------------------------------------------