diff --git a/.eslintignore b/.eslintignore
index 6758d7f68c..4447ad6726 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1,10 +1,8 @@
-app/assets/javascripts/browser-update.js
app/assets/javascripts/locales/i18n.js
app/assets/javascripts/ember-addons/
-app/assets/javascripts/discourse/lib/autosize.js
lib/javascripts/locale/
lib/javascripts/messageformat.js
-lib/highlight_js/
+lib/javascripts/messageformat-lookup.js
lib/pretty_text/
plugins/**/lib/javascripts/locale
public/
diff --git a/.github/labeler.yml b/.github/labeler.yml
new file mode 100644
index 0000000000..90492aebc0
--- /dev/null
+++ b/.github/labeler.yml
@@ -0,0 +1,2 @@
+chat:
+- plugins/chat/**/*
diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml
new file mode 100644
index 0000000000..057208eda3
--- /dev/null
+++ b/.github/workflows/labeler.yml
@@ -0,0 +1,14 @@
+name: "Pull Request Labeler"
+on:
+- pull_request_target
+
+jobs:
+ triage:
+ permissions:
+ contents: read
+ pull-requests: write
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/labeler@v4
+ with:
+ repo-token: "${{ secrets.GITHUB_TOKEN }}"
diff --git a/.github/workflows/licenses.yml b/.github/workflows/licenses.yml
index 18fa2b2f0e..7bedc1dfc9 100644
--- a/.github/workflows/licenses.yml
+++ b/.github/workflows/licenses.yml
@@ -52,7 +52,7 @@ jobs:
- name: Get yarn cache directory
id: yarn-cache-dir
- run: echo "::set-output name=dir::$(yarn cache dir)"
+ run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
- name: Yarn cache
uses: actions/cache@v3
diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml
index 59379f06af..2522c36d4f 100644
--- a/.github/workflows/linting.yml
+++ b/.github/workflows/linting.yml
@@ -49,7 +49,7 @@ jobs:
- name: Get yarn cache directory
id: yarn-cache-dir
- run: echo "::set-output name=dir::$(yarn cache dir)"
+ run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
- name: Yarn cache
uses: actions/cache@v3
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index f1356bf73b..042e994984 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -41,8 +41,6 @@ jobs:
target: plugins
- build_type: frontend
target: core # Handled by core_frontend_tests job (below)
- - build_type: system
- target: plugins # Enable once at least 1 plugin has system tests
steps:
- uses: actions/checkout@v3
@@ -83,7 +81,7 @@ jobs:
- name: Get yarn cache directory
id: yarn-cache-dir
- run: echo "::set-output name=dir::$(yarn cache dir)"
+ run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
- name: Yarn cache
uses: actions/cache@v3
@@ -178,7 +176,7 @@ jobs:
- name: Plugin System Tests
if: matrix.build_type == 'system' && matrix.target == 'plugins'
- run: bin/system_rspec plugins/*/spec/system
+ run: LOAD_PLUGINS=1 bin/system_rspec plugins/*/spec/system
- name: Upload failed system test screenshots
uses: actions/upload-artifact@v3
@@ -223,7 +221,7 @@ jobs:
TESTEM_FIREFOX_PATH: ${{ (matrix.browser == 'Firefox Evergreen') && '/opt/firefox-evergreen/firefox' }}
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
with:
fetch-depth: 1
@@ -234,7 +232,7 @@ jobs:
- name: Get yarn cache directory
id: yarn-cache-dir
- run: echo "::set-output name=dir::$(yarn cache dir)"
+ run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
- name: Yarn cache
uses: actions/cache@v3
diff --git a/.gitignore b/.gitignore
index 11f40d38b6..25f9373afc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -39,6 +39,7 @@
!/plugins/discourse-narrative-bot
!/plugins/discourse-presence
!/plugins/lazy-yt/
+!/plugins/chat/
!/plugins/poll/
!/plugins/styleguide
/plugins/*/auto_generated/
diff --git a/.streerc b/.streerc
new file mode 100644
index 0000000000..0bc4379d46
--- /dev/null
+++ b/.streerc
@@ -0,0 +1,2 @@
+--print-width=100
+--plugins=plugin/trailing_comma
diff --git a/Gemfile.lock b/Gemfile.lock
index 84fa798d48..29506c76cb 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -89,7 +89,7 @@ GEM
activesupport (>= 3.0.0)
uniform_notifier (~> 1.11)
byebug (11.1.3)
- capybara (3.37.1)
+ capybara (3.38.0)
addressable
matrix
mini_mime (>= 0.1.3)
@@ -145,7 +145,7 @@ GEM
sprockets (>= 3.3, < 4.1)
ember-source (2.18.2)
erubi (1.11.0)
- excon (0.93.1)
+ excon (0.94.0)
execjs (2.8.1)
exifr (1.3.10)
fabrication (2.30.0)
@@ -182,17 +182,17 @@ GEM
image_size (>= 1.5, < 4)
in_threads (~> 1.3)
progress (~> 3.0, >= 3.0.1)
- image_size (3.1.0)
+ image_size (3.2.0)
in_threads (1.6.0)
jmespath (1.6.1)
- jquery-rails (4.5.0)
+ jquery-rails (4.5.1)
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
json (2.6.2)
json-schema (3.0.0)
addressable (>= 2.8)
- json_schemer (0.2.22)
+ json_schemer (0.2.23)
ecma-re-validator (~> 0.3)
hana (~> 1.3)
regexp_parser (~> 2.0)
@@ -239,7 +239,8 @@ GEM
mini_suffix (0.3.3)
ffi (~> 1.9)
minitest (5.16.3)
- mocha (1.16.0)
+ mocha (2.0.2)
+ ruby2_keywords (>= 0.0.5)
msgpack (1.6.0)
multi_json (1.15.0)
multi_xml (0.6.0)
@@ -306,7 +307,7 @@ GEM
openssl (> 2.0, < 3.1)
optimist (3.0.1)
parallel (1.22.1)
- parallel_tests (3.13.0)
+ parallel_tests (4.0.0)
parallel
parser (3.1.2.1)
ast (~> 2.4.1)
@@ -328,7 +329,7 @@ GEM
rack (2.2.4)
rack-mini-profiler (3.0.0)
rack (>= 1.2.0)
- rack-protection (3.0.2)
+ rack-protection (3.0.3)
rack
rack-test (2.0.2)
rack (>= 1.3)
@@ -370,7 +371,7 @@ GEM
rack (>= 1.4)
rexml (3.2.5)
rinku (2.0.6)
- rotp (6.2.0)
+ rotp (6.2.1)
rqrcode (2.1.2)
chunky_png (~> 1.0)
rqrcode_core (~> 1.0)
@@ -406,7 +407,7 @@ GEM
json-schema (>= 2.2, < 4.0)
railties (>= 3.1, < 7.1)
rspec-core (>= 2.14)
- rubocop (1.37.1)
+ rubocop (1.38.0)
json (~> 2.3)
parallel (~> 1.10)
parser (>= 3.1.2.1)
@@ -421,7 +422,7 @@ GEM
rubocop-discourse (3.0)
rubocop (>= 1.1.0)
rubocop-rspec (>= 2.0.0)
- rubocop-rspec (2.14.2)
+ rubocop-rspec (2.15.0)
rubocop (~> 1.33)
ruby-prof (1.4.3)
ruby-progressbar (1.11.0)
@@ -442,7 +443,7 @@ GEM
sprockets (> 3.0)
sprockets-rails
tilt
- selenium-webdriver (4.5.0)
+ selenium-webdriver (4.6.1)
childprocess (>= 0.5, < 5.0)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0)
@@ -506,7 +507,7 @@ GEM
xpath (3.2.0)
nokogiri (~> 1.8)
yaml-lint (0.0.10)
- zeitwerk (2.6.3)
+ zeitwerk (2.6.6)
PLATFORMS
aarch64-linux
diff --git a/app/assets/javascripts/.licensee.json b/app/assets/javascripts/.licensee.json
index 1287b5fde6..b3518d7a13 100644
--- a/app/assets/javascripts/.licensee.json
+++ b/app/assets/javascripts/.licensee.json
@@ -21,8 +21,7 @@
"line-stream": "0.0.0",
"regenerator-transform": "0.10.1",
"source-map": "0.1.43",
- "sourcemap-validator": "1.1.1",
- "xmldom": "0.1.31"
+ "sourcemap-validator": "1.1.1"
},
"corrections": true,
"ignore": [
diff --git a/app/assets/javascripts/admin/addon/components/watched-word-form.js b/app/assets/javascripts/admin/addon/components/watched-word-form.js
index 1d6ba8ccba..f1b3138a78 100644
--- a/app/assets/javascripts/admin/addon/components/watched-word-form.js
+++ b/app/assets/javascripts/admin/addon/components/watched-word-form.js
@@ -2,7 +2,7 @@ import discourseComputed, { observes } from "discourse-common/utils/decorators";
import Component from "@ember/component";
import I18n from "I18n";
import WatchedWord from "admin/models/watched-word";
-import { equal } from "@ember/object/computed";
+import { equal, not } from "@ember/object/computed";
import { isEmpty } from "@ember/utils";
import { schedule } from "@ember/runloop";
import { inject as service } from "@ember/service";
@@ -16,7 +16,7 @@ export default Component.extend({
showMessage: false,
selectedTags: null,
isCaseSensitive: false,
-
+ submitDisabled: not("word"),
canReplace: equal("actionKey", "replace"),
canTag: equal("actionKey", "tag"),
canLink: equal("actionKey", "link"),
diff --git a/app/assets/javascripts/admin/addon/controllers/admin-permalinks.js b/app/assets/javascripts/admin/addon/controllers/admin-permalinks.js
index 07372d2f10..37f5126068 100644
--- a/app/assets/javascripts/admin/addon/controllers/admin-permalinks.js
+++ b/app/assets/javascripts/admin/addon/controllers/admin-permalinks.js
@@ -6,11 +6,13 @@ import discourseDebounce from "discourse-common/lib/debounce";
import { observes } from "discourse-common/utils/decorators";
import { clipboardCopy } from "discourse/lib/utilities";
import { inject as service } from "@ember/service";
+import { or } from "@ember/object/computed";
export default Controller.extend({
dialog: service(),
loading: false,
filter: null,
+ showSearch: or("model.length", "filter"),
_debouncedShow() {
Permalink.findAll(this.filter).then((result) => {
diff --git a/app/assets/javascripts/admin/addon/controllers/admin-plugins.js b/app/assets/javascripts/admin/addon/controllers/admin-plugins.js
index be663cadb1..34ffe45783 100644
--- a/app/assets/javascripts/admin/addon/controllers/admin-plugins.js
+++ b/app/assets/javascripts/admin/addon/controllers/admin-plugins.js
@@ -1,17 +1,27 @@
import { action } from "@ember/object";
import Controller from "@ember/controller";
-import discourseComputed from "discourse-common/utils/decorators";
+import { inject as service } from "@ember/service";
export default Controller.extend({
- @discourseComputed
- adminRoutes() {
+ router: service(),
+
+ get adminRoutes() {
+ return this.allAdminRoutes.filter((r) => this.routeExists(r.full_location));
+ },
+
+ get brokenAdminRoutes() {
+ return this.allAdminRoutes.filter(
+ (r) => !this.routeExists(r.full_location)
+ );
+ },
+
+ get allAdminRoutes() {
return this.model
+ .filter((p) => p?.enabled)
.map((p) => {
- if (p.get("enabled")) {
- return p.admin_route;
- }
+ return p.admin_route;
})
- .compact();
+ .filter(Boolean);
},
@action
@@ -21,4 +31,13 @@ export default Controller.extend({
adminDetail.classList.toggle(state);
});
},
+
+ routeExists(routeName) {
+ try {
+ this.router.urlFor(routeName);
+ return true;
+ } catch (e) {
+ return false;
+ }
+ },
});
diff --git a/app/assets/javascripts/admin/addon/controllers/modals/admin-start-backup.js b/app/assets/javascripts/admin/addon/controllers/modals/admin-start-backup.js
index 2934f251ee..f60908b0e2 100644
--- a/app/assets/javascripts/admin/addon/controllers/modals/admin-start-backup.js
+++ b/app/assets/javascripts/admin/addon/controllers/modals/admin-start-backup.js
@@ -1,9 +1,22 @@
import Controller, { inject as controller } from "@ember/controller";
+import discourseComputed from "discourse-common/utils/decorators";
import ModalFunctionality from "discourse/mixins/modal-functionality";
export default Controller.extend(ModalFunctionality, {
adminBackupsLogs: controller(),
+ @discourseComputed
+ warningMessage() {
+ // this is never shown here, but we may want to show different
+ // messages in plugins
+ return "";
+ },
+
+ @discourseComputed
+ yesLabel() {
+ return "yes_value";
+ },
+
actions: {
startBackupWithUploads() {
this.send("closeModal");
diff --git a/app/assets/javascripts/admin/addon/mixins/setting-component.js b/app/assets/javascripts/admin/addon/mixins/setting-component.js
index d651d1690c..6b9bfbda10 100644
--- a/app/assets/javascripts/admin/addon/mixins/setting-component.js
+++ b/app/assets/javascripts/admin/addon/mixins/setting-component.js
@@ -43,6 +43,7 @@ export default Mixin.create({
validationMessage: null,
isSecret: oneWay("setting.secret"),
setting: null,
+ attributeBindings: ["setting.setting:data-setting"],
@discourseComputed("buffered.value", "setting.value")
dirty(bufferVal, settingVal) {
@@ -136,6 +137,7 @@ export default Mixin.create({
"default_email_mailing_list_mode_frequency",
"default_email_previous_replies",
"default_email_in_reply_to",
+ "default_hide_profile_and_presence",
"default_other_new_topic_duration_minutes",
"default_other_auto_track_topics_after_msecs",
"default_other_notification_level_when_replying",
diff --git a/app/assets/javascripts/admin/addon/templates/components/permalink-form.hbs b/app/assets/javascripts/admin/addon/templates/components/permalink-form.hbs
index 705ac6894d..2e30cd353d 100644
--- a/app/assets/javascripts/admin/addon/templates/components/permalink-form.hbs
+++ b/app/assets/javascripts/admin/addon/templates/components/permalink-form.hbs
@@ -6,8 +6,8 @@
{{i18n "search.no_results"}} {{i18n "admin.permalink.no_permalinks"}}
-
-
+ {{else}}
+ {{#if this.filter}}
+ {{i18n "admin.permalink.url"}}
- {{i18n "admin.permalink.destination"}}
-
-
-
- {{#each this.model as |pl|}}
-
-
+ {{/each}}
+
+
-
-
- {{#if pl.topic_id}}
- {{pl.topic_title}}
- {{/if}}
- {{#if pl.post_id}}
- {{pl.post_topic_title}} #{{pl.post_number}}
- {{/if}}
- {{#if pl.category_id}}
- {{category-link pl.category}}
- {{/if}}
- {{#if pl.tag_id}}
- {{pl.tag_name}}
- {{/if}}
- {{#if pl.external_url}}
- {{#if pl.linkIsExternal}}
- {{d-icon "external-link-alt"}}
+
+
+
+
- {{else}}
- {{i18n "search.no_results"}}
- {{/if}}
+ {{#if pl.post_id}}
+ {{pl.post_topic_title}} #{{pl.post_number}}
+ {{/if}}
+ {{#if pl.category_id}}
+ {{category-link pl.category}}
+ {{/if}}
+ {{#if pl.tag_id}}
+ {{pl.tag_name}}
+ {{/if}}
+ {{#if pl.external_url}}
+ {{#if pl.linkIsExternal}}
+ {{d-icon "external-link-alt"}}
+ {{/if}}
+ {{pl.external_url}}
+ {{/if}}
+ {{i18n "admin.permalink.url"}}
+ {{i18n "admin.permalink.destination"}}
+
+
+
+ {{#each this.model as |pl|}}
+
+
- {{/each}}
-
-
+
+
+ {{#if pl.topic_id}}
+ {{pl.topic_title}}
{{/if}}
- {{pl.external_url}}
- {{/if}}
-
-
-
-
+
+
this is an emoji ![]()
this is an emoji ![]()
this is an emoji ![]()
this is an emoji ![]()
`,
+ `emoticons
`,
"emoticons are still supported"
);
testUnescape(
"With emoji :O: :frog: :smile:",
- `With emoji
`,
+ `With emoji
`,
"title with emoji"
);
testUnescape(
@@ -47,27 +47,27 @@ discourseModule("Unit | Utility | emoji", function () {
);
testUnescape(
"(:frog:) :)",
- `(
)
`,
+ `(
)
`,
"non-word characters allowed next to emoji"
);
testUnescape(
":smile: hi",
- `
hi`,
+ `
hi`,
"start of line"
);
testUnescape(
"hi :smile:",
- `hi
`,
+ `hi
`,
"end of line"
);
testUnescape(
"hi :blonde_woman:t4:",
- `hi
`,
+ `hi
`,
"support for skin tones"
);
testUnescape(
"hi :blonde_woman:t4: :blonde_man:t6:",
- `hi
`,
+ `hi
`,
"support for multiple skin tones"
);
testUnescape(
@@ -95,7 +95,7 @@ discourseModule("Unit | Utility | emoji", function () {
);
testUnescape(
"Hello 😊 World",
- `Hello
World`,
+ `Hello
World`,
"emoji from Unicode emoji"
);
testUnescape(
@@ -108,7 +108,7 @@ discourseModule("Unit | Utility | emoji", function () {
);
testUnescape(
"Hello😊World",
- `Hello
World`,
+ `Hello
World`,
"emoji from Unicode emoji when inline translation enabled",
{
enable_inline_emoji_translation: true,
@@ -124,13 +124,13 @@ discourseModule("Unit | Utility | emoji", function () {
);
testUnescape(
"hi:smile:",
- `hi
`,
+ `hi
`,
"emoji when inline translation enabled",
{ enable_inline_emoji_translation: true }
);
assert.strictEqual(
emojiUnescape(":smile:", { tabIndex: "0" }),
- `
`,
+ `
`,
"emoji when tabindex is enabled"
);
});
diff --git a/app/assets/javascripts/discourse/tests/unit/lib/formatter-test.js b/app/assets/javascripts/discourse/tests/unit/lib/formatter-test.js
index 311fc2d763..415f5dade8 100644
--- a/app/assets/javascripts/discourse/tests/unit/lib/formatter-test.js
+++ b/app/assets/javascripts/discourse/tests/unit/lib/formatter-test.js
@@ -150,6 +150,7 @@ discourseModule("Unit | Utility | formatter", function (hooks) {
test("formatting tiny dates", function (assert) {
let shortDateYear = shortDateTester("MMM 'YY");
+ this.siteSettings.relative_date_duration = 14;
assert.strictEqual(formatMins(0), "1m");
assert.strictEqual(formatMins(1), "1m");
diff --git a/app/assets/javascripts/discourse/tests/unit/lib/link-lookup-test.js b/app/assets/javascripts/discourse/tests/unit/lib/link-lookup-test.js
index 9947393575..e1454fd4ae 100644
--- a/app/assets/javascripts/discourse/tests/unit/lib/link-lookup-test.js
+++ b/app/assets/javascripts/discourse/tests/unit/lib/link-lookup-test.js
@@ -1,10 +1,11 @@
import LinkLookup from "discourse/lib/link-lookup";
import { module, test } from "qunit";
-import Post from "discourse/models/post";
+import { getOwner } from "discourse-common/lib/get-owner";
module("Unit | Utility | link-lookup", function (hooks) {
hooks.beforeEach(function () {
- this.post = Post.create();
+ const store = getOwner(this).lookup("service:store");
+ this.post = store.createRecord("post");
this.linkLookup = new LinkLookup({
"en.wikipedia.org/wiki/handheld_game_console": {
post_number: 1,
diff --git a/app/assets/javascripts/discourse/tests/unit/lib/plugin-api-test.js b/app/assets/javascripts/discourse/tests/unit/lib/plugin-api-test.js
new file mode 100644
index 0000000000..a8b6b8f5d5
--- /dev/null
+++ b/app/assets/javascripts/discourse/tests/unit/lib/plugin-api-test.js
@@ -0,0 +1,87 @@
+import { discourseModule } from "discourse/tests/helpers/qunit-helpers";
+import { test } from "qunit";
+import EmberObject from "@ember/object";
+import discourseComputed from "discourse-common/utils/decorators";
+import { withPluginApi } from "discourse/lib/plugin-api";
+
+discourseModule("Unit | Utility | plugin-api", function () {
+ test("modifyClass works with classic Ember objects", function (assert) {
+ const TestThingy = EmberObject.extend({
+ @discourseComputed
+ prop() {
+ return "hello";
+ },
+ });
+
+ this.registry.register("test-thingy:main", TestThingy);
+
+ withPluginApi("1.1.0", (api) => {
+ api.modifyClass("test-thingy:main", {
+ pluginId: "plugin-api-test",
+
+ @discourseComputed
+ prop() {
+ return `${this._super(...arguments)} there`;
+ },
+ });
+ });
+
+ const thingy = this.container.lookup("test-thingy:main");
+ assert.strictEqual(thingy.prop, "hello there");
+ });
+
+ test("modifyClass works with native class Ember objects", function (assert) {
+ class NativeTestThingy extends EmberObject {
+ @discourseComputed
+ prop() {
+ return "howdy";
+ }
+ }
+
+ this.registry.register("native-test-thingy:main", NativeTestThingy);
+
+ withPluginApi("1.1.0", (api) => {
+ api.modifyClass("native-test-thingy:main", {
+ pluginId: "plugin-api-test",
+
+ @discourseComputed
+ prop() {
+ return `${this._super(...arguments)} partner`;
+ },
+ });
+ });
+
+ const thingy = this.container.lookup("native-test-thingy:main");
+ assert.strictEqual(thingy.prop, "howdy partner");
+ });
+
+ test("modifyClass works with native classes", function (assert) {
+ class ClassTestThingy {
+ get keep() {
+ return "hey!";
+ }
+
+ get prop() {
+ return "top of the morning";
+ }
+ }
+
+ this.registry.register("class-test-thingy:main", new ClassTestThingy(), {
+ instantiate: false,
+ });
+
+ withPluginApi("1.1.0", (api) => {
+ api.modifyClass("class-test-thingy:main", {
+ pluginId: "plugin-api-test",
+
+ get prop() {
+ return "g'day";
+ },
+ });
+ });
+
+ const thingy = this.container.lookup("class-test-thingy:main");
+ assert.strictEqual(thingy.keep, "hey!");
+ assert.strictEqual(thingy.prop, "g'day");
+ });
+});
diff --git a/app/assets/javascripts/discourse/tests/unit/lib/pretty-text-test.js b/app/assets/javascripts/discourse/tests/unit/lib/pretty-text-test.js
index b0c2ead7d9..4e7e824722 100644
--- a/app/assets/javascripts/discourse/tests/unit/lib/pretty-text-test.js
+++ b/app/assets/javascripts/discourse/tests/unit/lib/pretty-text-test.js
@@ -4,19 +4,19 @@ import {
deleteCachedInlineOnebox,
} from "pretty-text/inline-oneboxer";
import QUnit, { module, test } from "qunit";
-import Post from "discourse/models/post";
import { buildQuote } from "discourse/lib/quote";
import { deepMerge } from "discourse-common/lib/object";
import { extractDataAttribute } from "pretty-text/engines/discourse-markdown-it";
import { registerEmoji } from "pretty-text/emoji";
import { IMAGE_VERSION as v } from "pretty-text/emoji/version";
+import { getOwner } from "discourse-common/lib/get-owner";
const rawOpts = {
siteSettings: {
enable_emoji: true,
enable_emoji_shortcuts: true,
enable_mentions: true,
- emoji_set: "google_classic",
+ emoji_set: "twitter",
external_emoji_url: "",
highlighted_languages: "json|ruby|javascript",
default_code_lang: "auto",
@@ -1274,7 +1274,8 @@ eviltrout
});
test("quotes", function (assert) {
- const post = Post.create({
+ const store = getOwner(this).lookup("service:store");
+ const post = store.createRecord("post", {
cooked: "lorem ipsum
", username: "eviltrout", post_number: 1, @@ -1334,7 +1335,8 @@ eviltrout }); test("quoting a quote", function (assert) { - const post = Post.create({ + const store = getOwner(this).lookup("service:store"); + const post = store.createRecord("post", { cooked: new PrettyText(defaultOpts).cook( '[quote="sam, post:1, topic:1, full:true"]\nhello\n[/quote]\n*Test*' ), @@ -1524,7 +1526,7 @@ var bar = 'bar'; assert.cookedOptions( ":grin: @sam", { featuresOverride: ["emoji"] }, - `
@sam
@sam
![]()
![]()
![]()
![]()
![]()
![]()
test
test
test
test
with all
the emojis 
`,
+ `
with all
the emojis 
`,
"supports emojis"
);
});
test("fancyTitle direction", function (assert) {
- const rtlTopic = Topic.create({ fancy_title: "هذا اختبار" });
- const ltrTopic = Topic.create({ fancy_title: "This is a test" });
+ const rtlTopic = this.store.createRecord("topic", {
+ fancy_title: "هذا اختبار",
+ });
+ const ltrTopic = this.store.createRecord("topic", {
+ fancy_title: "This is a test",
+ });
this.siteSettings.support_mixed_text_direction = true;
assert.strictEqual(
@@ -166,28 +231,28 @@ discourseModule("Unit | Model | topic", function () {
});
test("excerpt", function (assert) {
- const topic = Topic.create({
+ const topic = this.store.createRecord("topic", {
excerpt: "This is a test topic :smile:",
pinned: true,
});
assert.strictEqual(
topic.get("escapedExcerpt"),
- `This is a test topic
`,
+ `This is a test topic
`,
"supports emojis"
);
});
test("visible & invisible", function (assert) {
- const topic = Topic.create();
+ const topic = this.store.createRecord("topic");
assert.strictEqual(topic.visible, undefined);
assert.strictEqual(topic.invisible, undefined);
- const visibleTopic = Topic.create({ visible: true });
+ const visibleTopic = this.store.createRecord("topic", { visible: true });
assert.strictEqual(visibleTopic.visible, true);
assert.strictEqual(visibleTopic.invisible, false);
- const invisibleTopic = Topic.create({ visible: false });
+ const invisibleTopic = this.store.createRecord("topic", { visible: false });
assert.strictEqual(invisibleTopic.visible, false);
assert.strictEqual(invisibleTopic.invisible, true);
});
diff --git a/app/assets/javascripts/discourse/tests/unit/models/topic-tracking-state-test.js b/app/assets/javascripts/discourse/tests/unit/models/topic-tracking-state-test.js
index e37011fec3..482ea1a0d8 100644
--- a/app/assets/javascripts/discourse/tests/unit/models/topic-tracking-state-test.js
+++ b/app/assets/javascripts/discourse/tests/unit/models/topic-tracking-state-test.js
@@ -11,13 +11,14 @@ import {
import { NotificationLevels } from "discourse/lib/notification-levels";
import TopicTrackingState from "discourse/models/topic-tracking-state";
import User from "discourse/models/user";
-import Topic from "discourse/models/topic";
import createStore from "discourse/tests/helpers/create-store";
import sinon from "sinon";
+import { getOwner } from "discourse-common/lib/get-owner";
discourseModule("Unit | Model | topic-tracking-state", function (hooks) {
hooks.beforeEach(function () {
this.clock = fakeTime("2012-12-31 12:00");
+ this.store = getOwner(this).lookup("service:store");
});
hooks.afterEach(function () {
@@ -103,6 +104,16 @@ discourseModule("Unit | Model | topic-tracking-state", function (hooks) {
0,
"pending tag new counts"
);
+
+ // Ensure it is not throwing an error when filterTag is set and message payload is missing tags
+ trackingState.trackIncoming("tag/test/l/latest");
+ trackingState.notifyIncoming({
+ message_type: "new_topic",
+ topic_id: 4,
+ payload: { category_id: 2 },
+ });
+ const testTagCount = trackingState.countTags(["test"]);
+ assert.strictEqual(testTagCount["test"].unreadCount, 0);
});
test("tag counts - with total", function (assert) {
@@ -295,7 +306,7 @@ discourseModule("Unit | Model | topic-tracking-state", function (hooks) {
trackingState.updateSeen(111, 7);
const list = {
topics: [
- Topic.create({
+ this.store.createRecord("topic", {
highest_post_number: null,
id: 111,
unread_posts: 10,
@@ -325,7 +336,7 @@ discourseModule("Unit | Model | topic-tracking-state", function (hooks) {
const list = {
topics: [
- Topic.create({
+ this.store.createRecord("topic", {
id: 111,
unseen: false,
seen: true,
@@ -366,12 +377,12 @@ discourseModule("Unit | Model | topic-tracking-state", function (hooks) {
const list = {
topics: [
- Topic.create({
+ this.store.createRecord("topic", {
id: 111,
last_read_post_number: null,
unseen: true,
}),
- Topic.create({
+ this.store.createRecord("topic", {
id: 222,
last_read_post_number: null,
unseen: true,
@@ -400,7 +411,7 @@ discourseModule("Unit | Model | topic-tracking-state", function (hooks) {
const list = {
topics: [
- Topic.create({
+ this.store.createRecord("topic", {
id: 111,
unseen: true,
seen: false,
@@ -409,7 +420,7 @@ discourseModule("Unit | Model | topic-tracking-state", function (hooks) {
category_id: 1,
tags: ["pending"],
}),
- Topic.create({
+ this.store.createRecord("topic", {
id: 222,
unseen: false,
seen: true,
@@ -588,12 +599,12 @@ discourseModule("Unit | Model | topic-tracking-state", function (hooks) {
assert.strictEqual(trackingState.filterTag, "test");
assert.strictEqual(trackingState.filter, "latest");
- trackingState.trackIncoming("c/cat/subcat/6/l/latest");
+ trackingState.trackIncoming("c/cat/sub-cat/6/l/latest");
assert.strictEqual(trackingState.filterCategory.id, 6);
assert.strictEqual(trackingState.filterTag, undefined);
assert.strictEqual(trackingState.filter, "latest");
- trackingState.trackIncoming("tags/c/cat/subcat/6/test/l/latest");
+ trackingState.trackIncoming("tags/c/cat/sub-cat/6/test/l/latest");
assert.strictEqual(trackingState.filterCategory.id, 6);
assert.strictEqual(trackingState.filterTag, "test");
assert.strictEqual(trackingState.filter, "latest");
diff --git a/app/assets/javascripts/discourse/tests/unit/utils/decorators-test.js b/app/assets/javascripts/discourse/tests/unit/utils/decorators-test.js
index dc7598055e..1c07f7dcf5 100644
--- a/app/assets/javascripts/discourse/tests/unit/utils/decorators-test.js
+++ b/app/assets/javascripts/discourse/tests/unit/utils/decorators-test.js
@@ -57,12 +57,18 @@ class NativeComponent extends Component {
const TestStub = EmberObject.extend({
counter: 0,
otherCounter: 0,
+ state: null,
@debounce(50)
increment(value) {
this.counter += value;
},
+ @debounce(50, true)
+ setState(state) {
+ this.state = state;
+ },
+
// Note: it only works in this particular order:
// `@observes()` first, then `@debounce()`
@observes("prop")
@@ -149,6 +155,16 @@ module("Unit | Utils | decorators", function (hooks) {
assert.strictEqual(stub.counter, 6);
});
+ test("immediate debounce", async function (assert) {
+ const stub = TestStub.create();
+
+ stub.setState("foo");
+ stub.setState("bar");
+ await settled();
+
+ assert.strictEqual(stub.state, "foo");
+ });
+
test("debounce works with @observe", async function (assert) {
const stub = TestStub.create();
diff --git a/app/assets/javascripts/pretty-text/addon/engines/discourse-markdown-it.js b/app/assets/javascripts/pretty-text/addon/engines/discourse-markdown-it.js
index 8ce6442575..e0813c2310 100644
--- a/app/assets/javascripts/pretty-text/addon/engines/discourse-markdown-it.js
+++ b/app/assets/javascripts/pretty-text/addon/engines/discourse-markdown-it.js
@@ -385,6 +385,12 @@ function setupMarkdownEngine(opts, featureConfig) {
opts.pluginCallbacks.forEach(([feature, callback]) => {
if (featureConfig[feature]) {
+ if (callback === null || callback === undefined) {
+ // eslint-disable-next-line no-console
+ console.log("BAD MARKDOWN CALLBACK FOUND");
+ // eslint-disable-next-line no-console
+ console.log(`FEATURE IS: ${feature}`);
+ }
opts.engine.use(callback);
}
});
diff --git a/app/assets/javascripts/pretty-text/package.json b/app/assets/javascripts/pretty-text/package.json
index c2817a55ca..04de7a4790 100644
--- a/app/assets/javascripts/pretty-text/package.json
+++ b/app/assets/javascripts/pretty-text/package.json
@@ -34,7 +34,7 @@
"ember-disable-prototype-extensions": "^1.1.3",
"ember-load-initializers": "^2.1.1",
"ember-resolver": "^8.0.3",
- "ember-source": "~3.28.8",
+ "ember-source": "~3.28.10",
"ember-source-channel-url": "^3.0.0",
"loader.js": "^4.7.0"
},
diff --git a/app/assets/javascripts/select-kit/addon/components/select-kit.js b/app/assets/javascripts/select-kit/addon/components/select-kit.js
index 11348538cd..316e80749c 100644
--- a/app/assets/javascripts/select-kit/addon/components/select-kit.js
+++ b/app/assets/javascripts/select-kit/addon/components/select-kit.js
@@ -283,6 +283,7 @@ export default Component.extend(
closeOnChange: true,
limitMatches: null,
placement: isDocumentRTL() ? "bottom-end" : "bottom-start",
+ verticalOffset: 3,
filterComponent: "select-kit/select-kit-filter",
selectedNameComponent: "selected-name",
selectedChoiceComponent: "selected-choice",
@@ -898,7 +899,7 @@ export default Component.extend(
{
name: "offset",
options: {
- offset: [0, 3],
+ offset: [0, this.selectKit.options.verticalOffset],
},
},
{
diff --git a/app/assets/javascripts/select-kit/package.json b/app/assets/javascripts/select-kit/package.json
index eeaba25d81..9d369ac410 100644
--- a/app/assets/javascripts/select-kit/package.json
+++ b/app/assets/javascripts/select-kit/package.json
@@ -33,7 +33,7 @@
"ember-disable-prototype-extensions": "^1.1.3",
"ember-load-initializers": "^2.1.1",
"ember-resolver": "^8.0.3",
- "ember-source": "~3.28.8",
+ "ember-source": "~3.28.10",
"ember-source-channel-url": "^3.0.0",
"loader.js": "^4.7.0"
},
diff --git a/app/assets/javascripts/service-worker.js.erb b/app/assets/javascripts/service-worker.js.erb
index 8580e6e6fc..e54831fef0 100644
--- a/app/assets/javascripts/service-worker.js.erb
+++ b/app/assets/javascripts/service-worker.js.erb
@@ -8,6 +8,8 @@ workbox.setConfig({
});
var authUrls = ["auth", "session/sso_login", "session/sso"].map(path => `<%= Discourse.base_path %>/${path}`);
+var chatRegex = /\/chat\/channel\/(\d+)\//;
+var inlineReplyIcon = "<%= UrlHelper.absolute("/images/push-notifications/inline_reply.png") %>";
var cacheVersion = "1";
var discourseCacheName = "discourse-" + cacheVersion;
@@ -134,6 +136,16 @@ function showNotification(title, body, icon, badge, tag, baseUrl, url) {
tag: tag
}
+ if (chatRegex.test(url)) {
+ notificationOptions['actions'] = [{
+ action: "reply",
+ title: "Reply",
+ placeholder: "reply",
+ type: "text",
+ icon: inlineReplyIcon
+ }];
+ }
+
return self.registration.showNotification(title, notificationOptions);
}
@@ -163,18 +175,51 @@ self.addEventListener('notificationclick', function(event) {
var url = event.notification.data.url;
var baseUrl = event.notification.data.baseUrl;
- // This looks to see if the current window is already open and
- // focuses if it is
- event.waitUntil(
- clients.matchAll({ type: "window" })
- .then(function(clientList) {
- var reusedClientWindow = clientList.some(function(client) {
- if (client.url === baseUrl + url && 'focus' in client) {
+ if (event.action === "reply") {
+ let csrf;
+ fetch("/session/csrf", {
+ credentials: "include",
+ headers: {
+ Accept: "application/json",
+ },
+ })
+ .then((response) => {
+ if (!response.ok) {
+ throw new Error("Network response was not OK");
+ }
+ return response.json();
+ })
+ .then((data) => {
+ csrf = data.csrf;
+
+ let chatTest = url.match(chatRegex);
+ if (chatTest.length > 0) {
+ let chatChannel = chatTest[1];
+
+ fetch(`${baseUrl}/chat/${chatChannel}.json`, {
+ credentials: "include",
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
+ "X-CSRF-Token": csrf,
+ },
+ body: `message=${event.reply}`,
+ method: "POST",
+ mode: "cors",
+ });
+ }
+ });
+ } else {
+ // This looks to see if the current window is already open and
+ // focuses if it is
+ event.waitUntil(
+ clients.matchAll({ type: "window" }).then(function (clientList) {
+ var reusedClientWindow = clientList.some(function (client) {
+ if (client.url === baseUrl + url && "focus" in client) {
client.focus();
return true;
}
- if ('postMessage' in client && 'focus' in client) {
+ if ("postMessage" in client && "focus" in client) {
client.focus();
client.postMessage({ url: url });
return true;
@@ -182,9 +227,11 @@ self.addEventListener('notificationclick', function(event) {
return false;
});
- if (!reusedClientWindow && clients.openWindow) return clients.openWindow(baseUrl + url);
+ if (!reusedClientWindow && clients.openWindow)
+ return clients.openWindow(baseUrl + url);
})
- );
+ );
+ }
});
self.addEventListener('message', function(event) {
diff --git a/app/assets/javascripts/truth-helpers/package.json b/app/assets/javascripts/truth-helpers/package.json
index bf2bbc2606..ffe17088c1 100644
--- a/app/assets/javascripts/truth-helpers/package.json
+++ b/app/assets/javascripts/truth-helpers/package.json
@@ -33,7 +33,7 @@
"ember-disable-prototype-extensions": "^1.1.3",
"ember-load-initializers": "^2.1.1",
"ember-resolver": "^8.0.3",
- "ember-source": "~3.28.8",
+ "ember-source": "~3.28.10",
"ember-source-channel-url": "^3.0.0",
"loader.js": "^4.7.0"
},
diff --git a/app/assets/javascripts/wizard/package.json b/app/assets/javascripts/wizard/package.json
index 6c0f1c383f..79c915026a 100644
--- a/app/assets/javascripts/wizard/package.json
+++ b/app/assets/javascripts/wizard/package.json
@@ -34,7 +34,7 @@
"ember-disable-prototype-extensions": "^1.1.3",
"ember-load-initializers": "^2.1.1",
"ember-resolver": "^8.0.3",
- "ember-source": "~3.28.8",
+ "ember-source": "~3.28.10",
"ember-source-channel-url": "^3.0.0",
"loader.js": "^4.7.0"
},
diff --git a/app/assets/javascripts/yarn.lock b/app/assets/javascripts/yarn.lock
index 4dee6eb138..6c9a00b534 100644
--- a/app/assets/javascripts/yarn.lock
+++ b/app/assets/javascripts/yarn.lock
@@ -18,42 +18,42 @@
"@babel/highlight" "^7.18.6"
"@babel/compat-data@^7.13.11", "@babel/compat-data@^7.16.4", "@babel/compat-data@^7.16.8":
- version "7.18.8"
- resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.18.8.tgz#2483f565faca607b8535590e84e7de323f27764d"
- integrity sha512-HSmX4WZPPK3FUxYp7g2T6EyO8j96HlZJlxmKPSh6KAcqwyDrfx7hKjXpAW/0FhFfTJsR0Yt4lAjLI2coMptIHQ==
-
-"@babel/compat-data@^7.19.3":
version "7.19.3"
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.19.3.tgz#707b939793f867f5a73b2666e6d9a3396eb03151"
integrity sha512-prBHMK4JYYK+wDjJF1q99KK4JLL+egWS4nmNqdlMUgCExMZ+iZW0hGhyC3VEbsPjvaN0TBhW//VIFwBrk8sEiw==
-"@babel/core@^7.1.6", "@babel/core@^7.12.0", "@babel/core@^7.13.8", "@babel/core@^7.16.7", "@babel/core@^7.19.6", "@babel/core@^7.3.4":
- version "7.19.6"
- resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.19.6.tgz#7122ae4f5c5a37c0946c066149abd8e75f81540f"
- integrity sha512-D2Ue4KHpc6Ys2+AxpIx1BZ8+UegLLLE2p3KJEuJRKmokHOtl49jQ5ny1773KsGLZs8MQvBidAF6yWUJxRqtKtg==
+"@babel/compat-data@^7.20.0":
+ version "7.20.1"
+ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.20.1.tgz#f2e6ef7790d8c8dbf03d379502dcc246dcce0b30"
+ integrity sha512-EWZ4mE2diW3QALKvDMiXnbZpRvlj+nayZ112nK93SnhqOtpdsbVD4W+2tEoT3YNBAG9RBR0ISY758ZkOgsn6pQ==
+
+"@babel/core@^7.1.6", "@babel/core@^7.12.0", "@babel/core@^7.13.8", "@babel/core@^7.16.7", "@babel/core@^7.20.2", "@babel/core@^7.3.4":
+ version "7.20.2"
+ resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.20.2.tgz#8dc9b1620a673f92d3624bd926dc49a52cf25b92"
+ integrity sha512-w7DbG8DtMrJcFOi4VrLm+8QM4az8Mo+PuLBKLp2zrYRCow8W/f9xiXm5sN53C8HksCyDQwCKha9JiDoIyPjT2g==
dependencies:
"@ampproject/remapping" "^2.1.0"
"@babel/code-frame" "^7.18.6"
- "@babel/generator" "^7.19.6"
- "@babel/helper-compilation-targets" "^7.19.3"
- "@babel/helper-module-transforms" "^7.19.6"
- "@babel/helpers" "^7.19.4"
- "@babel/parser" "^7.19.6"
+ "@babel/generator" "^7.20.2"
+ "@babel/helper-compilation-targets" "^7.20.0"
+ "@babel/helper-module-transforms" "^7.20.2"
+ "@babel/helpers" "^7.20.1"
+ "@babel/parser" "^7.20.2"
"@babel/template" "^7.18.10"
- "@babel/traverse" "^7.19.6"
- "@babel/types" "^7.19.4"
+ "@babel/traverse" "^7.20.1"
+ "@babel/types" "^7.20.2"
convert-source-map "^1.7.0"
debug "^4.1.0"
gensync "^1.0.0-beta.2"
json5 "^2.2.1"
semver "^6.3.0"
-"@babel/generator@^7.19.6":
- version "7.19.6"
- resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.19.6.tgz#9e481a3fe9ca6261c972645ae3904ec0f9b34a1d"
- integrity sha512-oHGRUQeoX1QrKeJIKVe0hwjGqNnVYsM5Nep5zo0uE0m42sLH+Fsd2pStJ5sRM1bNyTUUoz0pe2lTeMJrb/taTA==
+"@babel/generator@^7.20.1", "@babel/generator@^7.20.2":
+ version "7.20.3"
+ resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.20.3.tgz#e58c9ae2f7bf7fdf4899160cf1e04400a82cd641"
+ integrity sha512-Wl5ilw2UD1+ZYprHVprxHZJCFeBWlzZYOovE4SDYLZnqCOD11j+0QzNeEWKLLTWM7nixrZEh7vNIyb76MyJg3A==
dependencies:
- "@babel/types" "^7.19.4"
+ "@babel/types" "^7.20.2"
"@jridgewell/gen-mapping" "^0.3.2"
jsesc "^2.5.1"
@@ -72,12 +72,12 @@
"@babel/helper-explode-assignable-expression" "^7.16.7"
"@babel/types" "^7.16.7"
-"@babel/helper-compilation-targets@^7.12.0", "@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.16.7", "@babel/helper-compilation-targets@^7.19.3":
- version "7.19.3"
- resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.19.3.tgz#a10a04588125675d7c7ae299af86fa1b2ee038ca"
- integrity sha512-65ESqLGyGmLvgR0mst5AdW1FkNlj9rQsCKduzEoEPhBCDFGXvz2jW6bXFG6i0/MrV2s7hhXjjb2yAzcPuQlLwg==
+"@babel/helper-compilation-targets@^7.12.0", "@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.16.7", "@babel/helper-compilation-targets@^7.20.0":
+ version "7.20.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.0.tgz#6bf5374d424e1b3922822f1d9bdaa43b1a139d0a"
+ integrity sha512-0jp//vDGp9e8hZzBc6N/KwA5ZK3Wsm/pfm4CrY7vzegkVxc65SgSn6wYOnwHe9Js9HRQ1YTCKLGPzDtaS3RoLQ==
dependencies:
- "@babel/compat-data" "^7.19.3"
+ "@babel/compat-data" "^7.20.0"
"@babel/helper-validator-option" "^7.18.6"
browserslist "^4.21.3"
semver "^6.3.0"
@@ -131,12 +131,7 @@
resolve "^1.14.2"
semver "^6.1.2"
-"@babel/helper-environment-visitor@^7.16.7":
- version "7.18.6"
- resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.6.tgz#b7eee2b5b9d70602e59d1a6cad7dd24de7ca6cd7"
- integrity sha512-8n6gSfn2baOY+qlp+VSzsosjCVGFqWKmDF0cCWOybh52Dw3SEyoWR1KrhMJASjLwIEkkAufZ0xvr+SxLHSpy2Q==
-
-"@babel/helper-environment-visitor@^7.18.9":
+"@babel/helper-environment-visitor@^7.16.7", "@babel/helper-environment-visitor@^7.18.9":
version "7.18.9"
resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be"
integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==
@@ -148,15 +143,7 @@
dependencies:
"@babel/types" "^7.16.7"
-"@babel/helper-function-name@^7.16.7":
- version "7.18.6"
- resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.18.6.tgz#8334fecb0afba66e6d87a7e8c6bb7fed79926b83"
- integrity sha512-0mWMxV1aC97dhjCah5U5Ua7668r5ZmSC2DLfH2EZnf9c3/dHZKiFa5pRLMH5tjSl471tY6496ZWk/kjNONBxhw==
- dependencies:
- "@babel/template" "^7.18.6"
- "@babel/types" "^7.18.6"
-
-"@babel/helper-function-name@^7.19.0":
+"@babel/helper-function-name@^7.16.7", "@babel/helper-function-name@^7.19.0":
version "7.19.0"
resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz#941574ed5390682e872e52d3f38ce9d1bef4648c"
integrity sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==
@@ -185,19 +172,19 @@
dependencies:
"@babel/types" "^7.18.6"
-"@babel/helper-module-transforms@^7.16.7", "@babel/helper-module-transforms@^7.19.6":
- version "7.19.6"
- resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.19.6.tgz#6c52cc3ac63b70952d33ee987cbee1c9368b533f"
- integrity sha512-fCmcfQo/KYr/VXXDIyd3CBGZ6AFhPFy1TfSEJ+PilGVlQT6jcbqtHAM4C1EciRqMza7/TpOUZliuSH+U6HAhJw==
+"@babel/helper-module-transforms@^7.16.7", "@babel/helper-module-transforms@^7.20.2":
+ version "7.20.2"
+ resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.20.2.tgz#ac53da669501edd37e658602a21ba14c08748712"
+ integrity sha512-zvBKyJXRbmK07XhMuujYoJ48B5yvvmM6+wcpv6Ivj4Yg6qO7NOZOSnvZN9CRl1zz1Z4cKf8YejmCMh8clOoOeA==
dependencies:
"@babel/helper-environment-visitor" "^7.18.9"
"@babel/helper-module-imports" "^7.18.6"
- "@babel/helper-simple-access" "^7.19.4"
+ "@babel/helper-simple-access" "^7.20.2"
"@babel/helper-split-export-declaration" "^7.18.6"
"@babel/helper-validator-identifier" "^7.19.1"
"@babel/template" "^7.18.10"
- "@babel/traverse" "^7.19.6"
- "@babel/types" "^7.19.4"
+ "@babel/traverse" "^7.20.1"
+ "@babel/types" "^7.20.2"
"@babel/helper-optimise-call-expression@^7.16.7":
version "7.16.7"
@@ -232,19 +219,19 @@
"@babel/types" "^7.16.7"
"@babel/helper-simple-access@^7.16.7":
- version "7.18.6"
- resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.18.6.tgz#d6d8f51f4ac2978068df934b569f08f29788c7ea"
- integrity sha512-iNpIgTgyAvDQpDj76POqg+YEt8fPxx3yaNBg3S30dxNKm2SWfYhD0TGrK/Eu9wHpUW63VQU894TsTg+GLbUa1g==
- dependencies:
- "@babel/types" "^7.18.6"
-
-"@babel/helper-simple-access@^7.19.4":
version "7.19.4"
resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.19.4.tgz#be553f4951ac6352df2567f7daa19a0ee15668e7"
integrity sha512-f9Xq6WqBFqaDfbCzn2w85hwklswz5qsKlh7f08w4Y9yhJHpnNC0QemtSkK5YyOY8kPGvyiwdzZksGUhnGdaUIg==
dependencies:
"@babel/types" "^7.19.4"
+"@babel/helper-simple-access@^7.20.2":
+ version "7.20.2"
+ resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz#0ab452687fe0c2cfb1e2b9e0015de07fc2d62dd9"
+ integrity sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==
+ dependencies:
+ "@babel/types" "^7.20.2"
+
"@babel/helper-skip-transparent-expression-wrappers@^7.16.0":
version "7.16.0"
resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.16.0.tgz#0ee3388070147c3ae051e487eca3ebb0e2e8bb09"
@@ -264,12 +251,7 @@
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz#38d3acb654b4701a9b77fb0615a96f775c3a9e63"
integrity sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==
-"@babel/helper-validator-identifier@^7.16.7", "@babel/helper-validator-identifier@^7.18.6":
- version "7.18.6"
- resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz#9c97e30d31b2b8c72a1d08984f2ca9b574d7a076"
- integrity sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==
-
-"@babel/helper-validator-identifier@^7.19.1":
+"@babel/helper-validator-identifier@^7.16.7", "@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1":
version "7.19.1"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2"
integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==
@@ -289,14 +271,14 @@
"@babel/traverse" "^7.16.8"
"@babel/types" "^7.16.8"
-"@babel/helpers@^7.19.4":
- version "7.19.4"
- resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.19.4.tgz#42154945f87b8148df7203a25c31ba9a73be46c5"
- integrity sha512-G+z3aOx2nfDHwX/kyVii5fJq+bgscg89/dJNWpYeKeBv3v9xX8EIabmx1k6u9LS04H7nROFVRVK+e3k0VHp+sw==
+"@babel/helpers@^7.20.1":
+ version "7.20.1"
+ resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.20.1.tgz#2ab7a0fcb0a03b5bf76629196ed63c2d7311f4c9"
+ integrity sha512-J77mUVaDTUJFZ5BpP6mMn6OIl3rEWymk2ZxDBQJUG3P+PbmyMcF3bYWvz0ma69Af1oobDqT/iAsvzhB58xhQUg==
dependencies:
"@babel/template" "^7.18.10"
- "@babel/traverse" "^7.19.4"
- "@babel/types" "^7.19.4"
+ "@babel/traverse" "^7.20.1"
+ "@babel/types" "^7.20.0"
"@babel/highlight@^7.18.6":
version "7.18.6"
@@ -307,10 +289,10 @@
chalk "^2.0.0"
js-tokens "^4.0.0"
-"@babel/parser@^7.18.10", "@babel/parser@^7.19.6", "@babel/parser@^7.4.5":
- version "7.19.6"
- resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.19.6.tgz#b923430cb94f58a7eae8facbffa9efd19130e7f8"
- integrity sha512-h1IUp81s2JYJ3mRkdxJgs4UvmSsRvDrx5ICSJbPvtWYv5i1nTBGcBpnog+89rAFMwvvru6E5NUHdBe01UeSzYA==
+"@babel/parser@^7.18.10", "@babel/parser@^7.20.1", "@babel/parser@^7.20.2", "@babel/parser@^7.4.5":
+ version "7.20.3"
+ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.20.3.tgz#5358cf62e380cf69efcb87a7bb922ff88bfac6e2"
+ integrity sha512-OP/s5a94frIPXwjzEcv5S/tpQfc6XhxYUnmWpgdqMWGgYCuErA3SzozaRAMQgSZWKeTJxht9aWAkUY+0UzvOFg==
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.16.7":
version "7.16.7"
@@ -985,12 +967,12 @@
dependencies:
regenerator-runtime "^0.13.4"
-"@babel/standalone@^7.20.0":
- version "7.20.0"
- resolved "https://registry.yarnpkg.com/@babel/standalone/-/standalone-7.20.0.tgz#0d5b9b57bde3923503e84a9ef1eef6da244b7d61"
- integrity sha512-8toFReoMyknVN538KZYS9HJLUlpvibQiPQqt8TYFeyV+FlZUmM8TG2zcS8q4vAijCRLoAKT1EzeBVvbxjMfi9A==
+"@babel/standalone@^7.20.4":
+ version "7.20.4"
+ resolved "https://registry.yarnpkg.com/@babel/standalone/-/standalone-7.20.4.tgz#eb48c8d43087e95f3795322c28d84577f881bb11"
+ integrity sha512-27bv4h47jbaFZ7+e7gT1VEo9PNL1ynxqUX6/BERLz1qxm/5gzpbcHX+47VnSeYHyEyGZkRznpSOd8zPBhiz6tw==
-"@babel/template@^7.16.7", "@babel/template@^7.18.10", "@babel/template@^7.18.6":
+"@babel/template@^7.16.7", "@babel/template@^7.18.10":
version "7.18.10"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.10.tgz#6f9134835970d1dbf0835c0d100c9f38de0c5e71"
integrity sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==
@@ -999,26 +981,26 @@
"@babel/parser" "^7.18.10"
"@babel/types" "^7.18.10"
-"@babel/traverse@^7.1.6", "@babel/traverse@^7.13.0", "@babel/traverse@^7.16.7", "@babel/traverse@^7.16.8", "@babel/traverse@^7.19.4", "@babel/traverse@^7.19.6", "@babel/traverse@^7.4.5":
- version "7.19.6"
- resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.19.6.tgz#7b4c865611df6d99cb131eec2e8ac71656a490dc"
- integrity sha512-6l5HrUCzFM04mfbG09AagtYyR2P0B71B1wN7PfSPiksDPz2k5H9CBC1tcZpz2M8OxbKTPccByoOJ22rUKbpmQQ==
+"@babel/traverse@^7.1.6", "@babel/traverse@^7.13.0", "@babel/traverse@^7.16.7", "@babel/traverse@^7.16.8", "@babel/traverse@^7.20.1", "@babel/traverse@^7.4.5":
+ version "7.20.1"
+ resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.20.1.tgz#9b15ccbf882f6d107eeeecf263fbcdd208777ec8"
+ integrity sha512-d3tN8fkVJwFLkHkBN479SOsw4DMZnz8cdbL/gvuDuzy3TS6Nfw80HuQqhw1pITbIruHyh7d1fMA47kWzmcUEGA==
dependencies:
"@babel/code-frame" "^7.18.6"
- "@babel/generator" "^7.19.6"
+ "@babel/generator" "^7.20.1"
"@babel/helper-environment-visitor" "^7.18.9"
"@babel/helper-function-name" "^7.19.0"
"@babel/helper-hoist-variables" "^7.18.6"
"@babel/helper-split-export-declaration" "^7.18.6"
- "@babel/parser" "^7.19.6"
- "@babel/types" "^7.19.4"
+ "@babel/parser" "^7.20.1"
+ "@babel/types" "^7.20.0"
debug "^4.1.0"
globals "^11.1.0"
-"@babel/types@^7.1.6", "@babel/types@^7.16.0", "@babel/types@^7.16.7", "@babel/types@^7.16.8", "@babel/types@^7.18.10", "@babel/types@^7.18.6", "@babel/types@^7.19.0", "@babel/types@^7.19.4", "@babel/types@^7.4.4", "@babel/types@^7.7.2":
- version "7.19.4"
- resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.19.4.tgz#0dd5c91c573a202d600490a35b33246fed8a41c7"
- integrity sha512-M5LK7nAeS6+9j7hAq+b3fQs+pNfUtTGq+yFFfHnauFA8zQtLRfmuipmsKDKKLuyG+wC8ABW43A153YNawNTEtw==
+"@babel/types@^7.1.6", "@babel/types@^7.16.0", "@babel/types@^7.16.7", "@babel/types@^7.16.8", "@babel/types@^7.18.10", "@babel/types@^7.18.6", "@babel/types@^7.19.0", "@babel/types@^7.19.4", "@babel/types@^7.20.0", "@babel/types@^7.20.2", "@babel/types@^7.4.4", "@babel/types@^7.7.2":
+ version "7.20.2"
+ resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.20.2.tgz#67ac09266606190f496322dbaff360fdaa5e7842"
+ integrity sha512-FnnvsNWgZCr232sqtXggapvlkk/tuwR/qhGzcmxI0GXLCjmPYQPzio2FbdlWuY6y1sHFfQKk+rRbUZ9VStQMog==
dependencies:
"@babel/helper-string-parser" "^7.19.4"
"@babel/helper-validator-identifier" "^7.19.1"
@@ -1335,16 +1317,11 @@
"@nodelib/fs.stat" "2.0.5"
run-parallel "^1.1.9"
-"@nodelib/fs.stat@2.0.5":
+"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
version "2.0.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b"
integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
-"@nodelib/fs.stat@^2.0.2":
- version "2.0.4"
- resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz#a3f2dd61bab43b8db8fa108a121cfffe4c676655"
- integrity sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q==
-
"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8":
version "1.2.8"
resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a"
@@ -1363,26 +1340,40 @@
resolved "https://registry.yarnpkg.com/@simple-dom/interface/-/interface-1.4.0.tgz#e8feea579232017f89b0138e2726facda6fbb71f"
integrity sha512-l5qumKFWU0S+4ZzMaLXFU8tQZsicHEMEyAxI5kDFGhJsRqDwe0a7/iPA/GdxlGyDKseQQAgIz5kzU7eXTrlSpA==
-"@sinonjs/commons@^1.6.0", "@sinonjs/commons@^1.7.0", "@sinonjs/commons@^1.8.3":
+"@sinonjs/commons@^1.7.0":
version "1.8.3"
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.3.tgz#3802ddd21a50a949b6721ddd72da36e67e7f1b2d"
integrity sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==
dependencies:
type-detect "4.0.8"
-"@sinonjs/fake-timers@>=5", "@sinonjs/fake-timers@^9.1.2":
+"@sinonjs/commons@^2.0.0":
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-2.0.0.tgz#fd4ca5b063554307e8327b4564bd56d3b73924a3"
+ integrity sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==
+ dependencies:
+ type-detect "4.0.8"
+
+"@sinonjs/fake-timers@^7.0.4":
+ version "7.1.2"
+ resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz#2524eae70c4910edccf99b2f4e6efc5894aff7b5"
+ integrity sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==
+ dependencies:
+ "@sinonjs/commons" "^1.7.0"
+
+"@sinonjs/fake-timers@^9.1.2":
version "9.1.2"
resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz#4eaab737fab77332ab132d396a3c0d364bd0ea8c"
integrity sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==
dependencies:
"@sinonjs/commons" "^1.7.0"
-"@sinonjs/samsam@^6.1.1":
- version "6.1.1"
- resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-6.1.1.tgz#627f7f4cbdb56e6419fa2c1a3e4751ce4f6a00b1"
- integrity sha512-cZ7rKJTLiE7u7Wi/v9Hc2fs3Ucc3jrWeMgPHbbTCeVAB2S0wOBbYlkJVeNSL04i7fdhT8wIbDq1zhC/PXTD2SA==
+"@sinonjs/samsam@^7.0.1":
+ version "7.0.1"
+ resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-7.0.1.tgz#5b5fa31c554636f78308439d220986b9523fc51f"
+ integrity sha512-zsAk2Jkiq89mhZovB2LLOdTCxJF4hqqTToGP0ASWlhp4I1hqOjcfmZGafXntCN7MDC6yySH0mFHrYtHceOeLmw==
dependencies:
- "@sinonjs/commons" "^1.6.0"
+ "@sinonjs/commons" "^2.0.0"
lodash.get "^4.4.2"
type-detect "^4.0.8"
@@ -1391,6 +1382,11 @@
resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz#8da5c6530915653f3a1f38fd5f101d8c3f8079c5"
integrity sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==
+"@socket.io/component-emitter@~3.1.0":
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553"
+ integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==
+
"@tootallnate/once@2":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf"
@@ -1428,6 +1424,16 @@
dependencies:
"@types/node" "*"
+"@types/cookie@^0.4.1":
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d"
+ integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==
+
+"@types/cors@^2.8.12":
+ version "2.8.12"
+ resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.12.tgz#6b2c510a7ad7039e98e7b8d3d6598f4359e5c080"
+ integrity sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==
+
"@types/eslint-scope@^3.7.3":
version "3.7.4"
resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16"
@@ -1505,10 +1511,10 @@
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40"
integrity sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==
-"@types/node@*":
- version "14.14.37"
- resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.37.tgz#a3dd8da4eb84a996c36e331df98d82abd76b516e"
- integrity sha512-XYmBiy+ohOR4Lh5jE379fV2IU+6Jn4g5qASinhitfyO71b/sCo6MKsMLF5tc7Zf2CE8hViVQyYSobJNke8OvUw==
+"@types/node@*", "@types/node@>=10.0.0":
+ version "18.11.9"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.9.tgz#02d013de7058cea16d36168ef2fc653464cfbad4"
+ integrity sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==
"@types/qs@*":
version "6.9.6"
@@ -1875,6 +1881,11 @@
"@webassemblyjs/wast-parser" "1.9.0"
"@xtuc/long" "4.2.2"
+"@xmldom/xmldom@^0.8.0":
+ version "0.8.5"
+ resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.8.5.tgz#7f4b797cfda39355b512b4cfcc66b49b5d93d5f3"
+ integrity sha512-0dpjDLeCXYThL2YhqZcd/spuwoH+dmnFoND9ZxZkAYxp1IJUB2GP16ow2MJRsjVxy8j1Qv8BJRmN5GKnbDKCmQ==
+
"@xtuc/ieee754@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
@@ -1943,11 +1954,6 @@ acorn@^8.1.0, acorn@^8.7.1, acorn@^8.8.0:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8"
integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==
-after@0.8.2:
- version "0.8.2"
- resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f"
- integrity sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=
-
agent-base@6:
version "6.0.2"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
@@ -2096,16 +2102,16 @@ anymatch@~3.1.2:
normalize-path "^3.0.0"
picomatch "^2.0.4"
-aproba@^1.0.3, aproba@^1.1.1:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
- integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==
-
"aproba@^1.0.3 || ^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc"
integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==
+aproba@^1.1.1:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
+ integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==
+
are-we-there-yet@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz#679df222b278c64f2cdba1175cdc00b0d96164bd"
@@ -2114,14 +2120,6 @@ are-we-there-yet@^3.0.0:
delegates "^1.0.0"
readable-stream "^3.6.0"
-are-we-there-yet@~1.1.2:
- version "1.1.5"
- resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21"
- integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==
- dependencies:
- delegates "^1.0.0"
- readable-stream "^2.0.6"
-
argparse@^1.0.7:
version "1.0.10"
resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
@@ -2181,11 +2179,6 @@ array-unique@^0.3.2:
resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=
-arraybuffer.slice@~0.0.7:
- version "0.0.7"
- resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz#3bbc4275dd584cc1b10809b89d4e8b63a69e7675"
- integrity sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==
-
asn1.js@^5.2.0:
version "5.4.1"
resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07"
@@ -2576,27 +2569,17 @@ backbone@^1.1.2:
dependencies:
underscore ">=1.8.3"
-backo2@1.0.2:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947"
- integrity sha1-MasayLEpNjRj41s+u2n038+6eUc=
-
balanced-match@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
-base64-arraybuffer@0.1.4:
- version "0.1.4"
- resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz#9818c79e059b1355f97e0428a017c838e90ba812"
- integrity sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=
-
base64-js@^1.0.2:
version "1.5.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
-base64id@2.0.0:
+base64id@2.0.0, base64id@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6"
integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==
@@ -2653,12 +2636,7 @@ blank-object@^1.0.1:
resolved "https://registry.yarnpkg.com/blank-object/-/blank-object-1.0.2.tgz#f990793fbe9a8c8dd013fb3219420bec81d5f4b9"
integrity sha1-+ZB5P76ajI3QE/syGUIL7IHV9Lk=
-blob@0.0.5:
- version "0.0.5"
- resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.5.tgz#d680eeef25f8cd91ad533f5b01eed48e64caf683"
- integrity sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==
-
-bluebird@^3.1.1, bluebird@^3.4.6, bluebird@^3.5.5:
+bluebird@^3.4.6, bluebird@^3.5.5, bluebird@^3.7.2:
version "3.7.2"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
@@ -3282,18 +3260,7 @@ browserify-zlib@^0.2.0:
dependencies:
pako "~1.0.5"
-browserslist@^4.14.5, browserslist@^4.19.1:
- version "4.20.4"
- resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.20.4.tgz#98096c9042af689ee1e0271333dbc564b8ce4477"
- integrity sha512-ok1d+1WpnU24XYN7oC3QWgTyMhY/avPJ/r9T00xxvUOIparA/gc+UPUMaod3i+G6s+nI2nUb9xZ5k794uIwShw==
- dependencies:
- caniuse-lite "^1.0.30001349"
- electron-to-chromium "^1.4.147"
- escalade "^3.1.1"
- node-releases "^2.0.5"
- picocolors "^1.0.0"
-
-browserslist@^4.21.3:
+browserslist@^4.14.5, browserslist@^4.19.1, browserslist@^4.21.3:
version "4.21.3"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.3.tgz#5df277694eb3c48bc5c4b05af3e8b7e09c5a6d1a"
integrity sha512-898rgRXLAyRkM1GryrrBHGkqA5hlpkV5MhtZwg9QXeiyLUYs2k00Un05aX5l2/yJIOObYKOpS2JNo8nJDE7fWQ==
@@ -3422,11 +3389,6 @@ can-symlink@^1.0.0:
dependencies:
tmp "0.0.28"
-caniuse-lite@^1.0.30001349:
- version "1.0.30001357"
- resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001357.tgz#dec7fc4158ef6ad24690d0eec7b91f32b8cb1b5d"
- integrity sha512-b+KbWHdHePp+ZpNj+RDHFChZmuN+J5EvuQUlee9jOQIUAdhv9uvAZeEtUeLAknXbkiu1uxjQ9NLp1ie894CuWg==
-
caniuse-lite@^1.0.30001370:
version "1.0.30001399"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001399.tgz#1bf994ca375d7f33f8d01ce03b7d5139e8587873"
@@ -3642,11 +3604,6 @@ clone@^2.1.2:
resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=
-code-point-at@^1.0.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
- integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
-
coffee-script@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/coffee-script/-/coffee-script-1.2.0.tgz#b5e61e55f1ca8c4a9eb87d53aa0657ea43125b91"
@@ -3738,26 +3695,11 @@ commondir@^1.0.1:
resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=
-component-bind@1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/component-bind/-/component-bind-1.0.0.tgz#00c608ab7dcd93897c0009651b1d3a8e1e73bbd1"
- integrity sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=
-
-component-emitter@1.2.1:
- version "1.2.1"
- resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6"
- integrity sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=
-
-component-emitter@^1.2.1, component-emitter@~1.3.0:
+component-emitter@^1.2.1:
version "1.3.0"
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"
integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==
-component-inherit@0.0.3:
- version "0.0.3"
- resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143"
- integrity sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=
-
compressible@~2.0.16:
version "2.0.18"
resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba"
@@ -3820,7 +3762,7 @@ console-browserify@^1.1.0:
resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336"
integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==
-console-control-strings@^1.0.0, console-control-strings@^1.1.0, console-control-strings@~1.1.0:
+console-control-strings@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=
@@ -3836,12 +3778,12 @@ console-ui@^3.0.4, console-ui@^3.1.2:
ora "^3.4.0"
through2 "^3.0.1"
-consolidate@^0.15.1:
- version "0.15.1"
- resolved "https://registry.yarnpkg.com/consolidate/-/consolidate-0.15.1.tgz#21ab043235c71a07d45d9aad98593b0dba56bab7"
- integrity sha512-DW46nrsMJgy9kqAbPt5rKaCr7uFtpo4mSUvLHIUbJEjm0vo+aY5QLwBUq3FK4tRnJr/X0Psc0C4jf/h+HtXSMw==
+consolidate@^0.16.0:
+ version "0.16.0"
+ resolved "https://registry.yarnpkg.com/consolidate/-/consolidate-0.16.0.tgz#a11864768930f2f19431660a65906668f5fbdc16"
+ integrity sha512-Nhl1wzCslqXYTJVDyJCu3ODohy9OfBMB5uD2BiBTzd7w+QY0lBzafkR8y8755yMYHAaMD4NuzbAw03/xzfw+eQ==
dependencies:
- bluebird "^3.1.1"
+ bluebird "^3.7.2"
constants-browserify@^1.0.0:
version "1.0.0"
@@ -3934,6 +3876,14 @@ core-util-is@~1.0.0:
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
+cors@~2.8.5:
+ version "2.8.5"
+ resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29"
+ integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==
+ dependencies:
+ object-assign "^4"
+ vary "^1"
+
create-ecdh@^4.0.0:
version "4.0.4"
resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e"
@@ -4076,7 +4026,7 @@ debug@2.6.9, debug@^2.1.0, debug@^2.1.1, debug@^2.1.3, debug@^2.2.0, debug@^2.3.
dependencies:
ms "2.0.0"
-debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0, debug@^4.3.1, debug@^4.3.2:
+debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0, debug@^4.3.1, debug@^4.3.2, debug@~4.3.1, debug@~4.3.2:
version "4.3.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
@@ -4090,20 +4040,6 @@ debug@^3.0.1, debug@^3.1.0, debug@^3.1.1:
dependencies:
ms "^2.1.1"
-debug@~3.1.0:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
- integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
- dependencies:
- ms "2.0.0"
-
-debug@~4.1.0:
- version "4.1.1"
- resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
- integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==
- dependencies:
- ms "^2.1.1"
-
decimal.js@^10.4.1:
version "10.4.1"
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.1.tgz#be75eeac4a2281aace80c1a8753587c27ef053e7"
@@ -4295,11 +4231,6 @@ ee-first@1.1.1:
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
-electron-to-chromium@^1.4.147:
- version "1.4.161"
- resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.161.tgz#49cb5b35385bfee6cc439d0a04fbba7a7a7f08a1"
- integrity sha512-sTjBRhqh6wFodzZtc5Iu8/R95OkwaPNn7tj/TaDU5nu/5EFiQDtADGAXdR4tJcTEHlYfJpHqigzJqHvPgehP8A==
-
electron-to-chromium@^1.4.202:
version "1.4.250"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.250.tgz#e4535fc00d17b9a719bc688352c4a185acc2a347"
@@ -4938,10 +4869,10 @@ ember-source-channel-url@^3.0.0:
dependencies:
node-fetch "^2.6.0"
-ember-source@~3.28.8:
- version "3.28.9"
- resolved "https://registry.yarnpkg.com/ember-source/-/ember-source-3.28.9.tgz#804c56b2d71d3cc3decff15a3273bb35d668300a"
- integrity sha512-Fy7V3yvj+3oyo2+ke52aaihKMcFnnF7Oj9ixj547yzh2faqRfqouB5ZSiwXFH8rxw22rKaM8DiuQO4JN2Ay6xQ==
+ember-source@~3.28.10:
+ version "3.28.10"
+ resolved "https://registry.yarnpkg.com/ember-source/-/ember-source-3.28.10.tgz#f4be7e2852d421a558f686505748f4c88f6d6ae6"
+ integrity sha512-TH8ug2rRUq6pLwqjciwvnuF8GDKBXNW2v5mvDkkf+k5S84XVHPjn3K0q2uGaR2W/mCDYg+mGmqu/PIGy0STx9Q==
dependencies:
"@babel/helper-module-imports" "^7.8.3"
"@babel/plugin-transform-block-scoping" "^7.8.3"
@@ -5001,45 +4932,26 @@ end-of-stream@^1.0.0, end-of-stream@^1.1.0:
dependencies:
once "^1.4.0"
-engine.io-client@~3.5.0:
- version "3.5.1"
- resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.5.1.tgz#b500458a39c0cd197a921e0e759721a746d0bdb9"
- integrity sha512-oVu9kBkGbcggulyVF0kz6BV3ganqUeqXvD79WOFKa+11oK692w1NyFkuEj4xrkFRpZhn92QOqTk4RQq5LiBXbQ==
- dependencies:
- component-emitter "~1.3.0"
- component-inherit "0.0.3"
- debug "~3.1.0"
- engine.io-parser "~2.2.0"
- has-cors "1.1.0"
- indexof "0.0.1"
- parseqs "0.0.6"
- parseuri "0.0.6"
- ws "~7.4.2"
- xmlhttprequest-ssl "~1.5.4"
- yeast "0.1.2"
+engine.io-parser@~5.0.3:
+ version "5.0.4"
+ resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.0.4.tgz#0b13f704fa9271b3ec4f33112410d8f3f41d0fc0"
+ integrity sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg==
-engine.io-parser@~2.2.0:
- version "2.2.1"
- resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-2.2.1.tgz#57ce5611d9370ee94f99641b589f94c97e4f5da7"
- integrity sha512-x+dN/fBH8Ro8TFwJ+rkB2AmuVw9Yu2mockR/p3W8f8YtExwFgDvBDi0GWyb4ZLkpahtDGZgtr3zLovanJghPqg==
- dependencies:
- after "0.8.2"
- arraybuffer.slice "~0.0.7"
- base64-arraybuffer "0.1.4"
- blob "0.0.5"
- has-binary2 "~1.0.2"
-
-engine.io@~3.5.0:
- version "3.5.0"
- resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-3.5.0.tgz#9d6b985c8a39b1fe87cd91eb014de0552259821b"
- integrity sha512-21HlvPUKaitDGE4GXNtQ7PLP0Sz4aWLddMPw2VTyFz1FVZqu/kZsJUO8WNpKuE/OCL7nkfRaOui2ZCJloGznGA==
+engine.io@~6.2.0:
+ version "6.2.0"
+ resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.2.0.tgz#003bec48f6815926f2b1b17873e576acd54f41d0"
+ integrity sha512-4KzwW3F3bk+KlzSOY57fj/Jx6LyRQ1nbcyIadehl+AnXjKT7gDO0ORdRi/84ixvMKTym6ZKuxvbzN62HDDU1Lg==
dependencies:
+ "@types/cookie" "^0.4.1"
+ "@types/cors" "^2.8.12"
+ "@types/node" ">=10.0.0"
accepts "~1.3.4"
base64id "2.0.0"
cookie "~0.4.1"
- debug "~4.1.0"
- engine.io-parser "~2.2.0"
- ws "~7.4.2"
+ cors "~2.8.5"
+ debug "~4.3.1"
+ engine.io-parser "~5.0.3"
+ ws "~8.2.3"
enhanced-resolve@^4.0.0, enhanced-resolve@^4.5.0:
version "4.5.0"
@@ -6048,20 +5960,6 @@ gauge@^4.0.3:
strip-ansi "^6.0.1"
wide-align "^1.1.5"
-gauge@~2.7.3:
- version "2.7.4"
- resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
- integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=
- dependencies:
- aproba "^1.0.3"
- console-control-strings "^1.0.0"
- has-unicode "^2.0.0"
- object-assign "^4.1.0"
- signal-exit "^3.0.0"
- string-width "^1.0.1"
- strip-ansi "^3.0.1"
- wide-align "^1.1.0"
-
gensync@^1.0.0-beta.2:
version "1.0.0-beta.2"
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
@@ -6312,18 +6210,6 @@ has-bigints@^1.0.1:
resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113"
integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==
-has-binary2@~1.0.2:
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/has-binary2/-/has-binary2-1.0.3.tgz#7776ac627f3ea77250cfc332dab7ddf5e4f5d11d"
- integrity sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==
- dependencies:
- isarray "2.0.1"
-
-has-cors@1.1.0:
- version "1.1.0"
- resolved "https://registry.yarnpkg.com/has-cors/-/has-cors-1.1.0.tgz#5e474793f7ea9843d1bb99c23eef49ff126fff39"
- integrity sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=
-
has-flag@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
@@ -6346,7 +6232,7 @@ has-tostringtag@^1.0.0:
dependencies:
has-symbols "^1.0.2"
-has-unicode@^2.0.0, has-unicode@^2.0.1:
+has-unicode@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=
@@ -6627,11 +6513,6 @@ imurmurhash@^0.1.4:
resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
integrity sha1-khi5srkoojixPcT7a21XbyMUU+o=
-indexof@0.0.1:
- version "0.0.1"
- resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d"
- integrity sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=
-
individual@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/individual/-/individual-3.0.0.tgz#e7ca4f85f8957b018734f285750dc22ec2f9862d"
@@ -6871,13 +6752,6 @@ is-finite@^1.0.0:
resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.1.0.tgz#904135c77fb42c0641d6aa1bcdbc4daa8da082f3"
integrity sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==
-is-fullwidth-code-point@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb"
- integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs=
- dependencies:
- number-is-nan "^1.0.0"
-
is-fullwidth-code-point@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
@@ -7049,11 +6923,6 @@ isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
-isarray@2.0.1:
- version "2.0.1"
- resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.1.tgz#a37d94ed9cda2d59865c9f76fe596ee1f338741e"
- integrity sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=
-
isbinaryfile@^4.0.6:
version "4.0.6"
resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.6.tgz#edcb62b224e2b4710830b67498c8e4e5a4d2610b"
@@ -7371,9 +7240,9 @@ loader-runner@^4.2.0:
integrity sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==
loader-utils@^1.2.3, loader-utils@^1.4.0:
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613"
- integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.1.tgz#278ad7006660bccc4d2c0c1578e17c5c78d5c0e0"
+ integrity sha512-1Qo97Y2oKaU+Ro2xnDMR26g1BwMT29jNbem1EvcujW2jqt+j5COXyscjM7bLQkM9HaxI7pkWeW7gnI072yMI9Q==
dependencies:
big.js "^5.2.2"
emojis-list "^3.0.0"
@@ -7946,11 +7815,16 @@ minimist@^0.2.1:
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.2.1.tgz#827ba4e7593464e7c221e8c5bed930904ee2c455"
integrity sha512-GY8fANSrTMfBVfInqJAY41QkOM+upUTytK1jZ0c8+3HdHrJxBJ3rF5i9moClXTE8uUSnUo8cAsCoxDXvSY4DHg==
-minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5:
+minimist@^1.1.1, minimist@^1.2.5:
version "1.2.5"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
+minimist@^1.2.0:
+ version "1.2.7"
+ resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18"
+ integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==
+
minipass@^2.2.0:
version "2.9.0"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6"
@@ -8048,10 +7922,10 @@ ms@^2.1.1:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
-mustache@^3.0.0:
- version "3.2.1"
- resolved "https://registry.yarnpkg.com/mustache/-/mustache-3.2.1.tgz#89e78a9d207d78f2799b1e95764a25bf71a28322"
- integrity sha512-RERvMFdLpaFfSRIEe632yDm5nsd0SDKn8hGmcUwswnyiE5mtdZLDybtHAz6hjJhawokF0hXvGLtx9mrQfm6FkA==
+mustache@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/mustache/-/mustache-4.2.0.tgz#e5892324d60a12ec9c2a73359edca52972bf6f64"
+ integrity sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==
mute-stream@0.0.7:
version "0.0.7"
@@ -8120,21 +7994,21 @@ nice-try@^1.0.4:
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
-nise@^5.1.1:
- version "5.1.1"
- resolved "https://registry.yarnpkg.com/nise/-/nise-5.1.1.tgz#ac4237e0d785ecfcb83e20f389185975da5c31f3"
- integrity sha512-yr5kW2THW1AkxVmCnKEh4nbYkJdB3I7LUkiUgOvEkOp414mc2UMaHMA7pjq1nYowhdoJZGwEKGaQVbxfpWj10A==
+nise@^5.1.2:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/nise/-/nise-5.1.2.tgz#a7b8909c216b3491fd4fc0b124efb69f3939b449"
+ integrity sha512-+gQjFi8v+tkfCuSCxfURHLhRhniE/+IaYbIphxAN2JRR9SHKhY8hgXpaXiYfHdw+gcGe4buxgbprBQFab9FkhA==
dependencies:
- "@sinonjs/commons" "^1.8.3"
- "@sinonjs/fake-timers" ">=5"
+ "@sinonjs/commons" "^2.0.0"
+ "@sinonjs/fake-timers" "^7.0.4"
"@sinonjs/text-encoding" "^0.7.1"
just-extend "^4.0.2"
path-to-regexp "^1.7.0"
-node-fetch@^2.6.0, node-fetch@^2.6.6:
- version "2.6.6"
- resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.6.tgz#1751a7c01834e8e1697758732e9efb6eeadfaf89"
- integrity sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==
+node-fetch@^2.6.0, node-fetch@^2.6.7:
+ version "2.6.7"
+ resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
+ integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
dependencies:
whatwg-url "^5.0.0"
@@ -8177,23 +8051,18 @@ node-modules-path@^1.0.0:
resolved "https://registry.yarnpkg.com/node-modules-path/-/node-modules-path-1.0.2.tgz#e3acede9b7baf4bc336e3496b58e5b40d517056e"
integrity sha512-6Gbjq+d7uhkO7epaKi5DNgUJn7H0gEyA4Jg0Mo1uQOi3Rk50G83LtmhhFyw0LxnAFhtlspkiiw52ISP13qzcBg==
-node-notifier@^9.0.1:
- version "9.0.1"
- resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-9.0.1.tgz#cea837f4c5e733936c7b9005e6545cea825d1af4"
- integrity sha512-fPNFIp2hF/Dq7qLDzSg4vZ0J4e9v60gJR+Qx7RbjbWqzPDdEqeVpEx5CFeDAELIl+A/woaaNn1fQ5nEVerMxJg==
+node-notifier@^10.0.0:
+ version "10.0.1"
+ resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-10.0.1.tgz#0e82014a15a8456c4cfcdb25858750399ae5f1c7"
+ integrity sha512-YX7TSyDukOZ0g+gmzjB6abKu+hTGvO8+8+gIFDsRCU2t8fLV/P2unmt+LGFaIa4y64aX98Qksa97rgz4vMNeLQ==
dependencies:
growly "^1.3.0"
is-wsl "^2.2.0"
- semver "^7.3.2"
+ semver "^7.3.5"
shellwords "^0.1.1"
- uuid "^8.3.0"
+ uuid "^8.3.2"
which "^2.0.2"
-node-releases@^2.0.5:
- version "2.0.5"
- resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.5.tgz#280ed5bc3eba0d96ce44897d8aee478bfb3d9666"
- integrity sha512-U9h1NLROZTq9uE1SNffn6WuPDg8icmi3ns4rEl/oTfIle4iLjTliCzgTsbaIFMq/Xn078/lfY/BL0GWZ+psK4Q==
-
node-releases@^2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503"
@@ -8260,16 +8129,6 @@ npm-run-path@^4.0.0, npm-run-path@^4.0.1:
dependencies:
path-key "^3.0.0"
-npmlog@^4.0.0:
- version "4.1.2"
- resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
- integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==
- dependencies:
- are-we-there-yet "~1.1.2"
- console-control-strings "~1.1.0"
- gauge "~2.7.3"
- set-blocking "~2.0.0"
-
npmlog@^6.0.0:
version "6.0.2"
resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-6.0.2.tgz#c8166017a42f2dea92d6453168dd865186a70830"
@@ -8280,17 +8139,12 @@ npmlog@^6.0.0:
gauge "^4.0.3"
set-blocking "^2.0.0"
-number-is-nan@^1.0.0:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
- integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=
-
nwsapi@^2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.2.tgz#e5418863e7905df67d51ec95938d67bf801f0bb0"
integrity sha512-90yv+6538zuvUMnN+zCr8LuV6bPFdq50304114vJYJ8RDyK8D5O9Phpbd6SZWgI7PwzmmfN1upeOJlvybDSgCw==
-object-assign@4.1.1, object-assign@^4.1.0, object-assign@^4.1.1:
+object-assign@4.1.1, object-assign@^4, object-assign@^4.1.0, object-assign@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
@@ -8563,16 +8417,6 @@ parse5@^7.1.1:
dependencies:
entities "^4.4.0"
-parseqs@0.0.6:
- version "0.0.6"
- resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.6.tgz#8e4bb5a19d1cdc844a08ac974d34e273afa670d5"
- integrity sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w==
-
-parseuri@0.0.6:
- version "0.0.6"
- resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.6.tgz#e1496e829e3ac2ff47f39a4dd044b32823c4a25a"
- integrity sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow==
-
parseurl@~1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
@@ -9027,7 +8871,7 @@ raw-body@~1.1.0:
bytes "1"
string_decoder "0.10"
-"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.6:
+"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.6:
version "2.3.7"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
@@ -9305,7 +9149,7 @@ reusify@^1.0.4:
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
-rimraf@^2.2.8, rimraf@^2.3.4, rimraf@^2.4.3, rimraf@^2.4.4, rimraf@^2.5.3, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3:
+rimraf@^2.2.8, rimraf@^2.3.4, rimraf@^2.4.3, rimraf@^2.5.3, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3:
version "2.7.1"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
@@ -9422,10 +9266,10 @@ sane@^4.0.0, sane@^4.1.0:
minimist "^1.1.1"
walker "~1.0.5"
-sass@^1.55.0:
- version "1.55.0"
- resolved "https://registry.yarnpkg.com/sass/-/sass-1.55.0.tgz#0c4d3c293cfe8f8a2e8d3b666e1cf1bff8065d1c"
- integrity sha512-Pk+PMy7OGLs9WaxZGJMn7S96dvlyVBwwtToX895WmCpAOr5YiJYEUJfiJidMuKb613z2xNWcXCHEuOvjZbqC6A==
+sass@^1.56.0:
+ version "1.56.0"
+ resolved "https://registry.yarnpkg.com/sass/-/sass-1.56.0.tgz#134032075a3223c8d49cb5c35e091e5ba1de8e0a"
+ integrity sha512-WFJ9XrpkcnqZcYuLRJh5qiV6ibQOR4AezleeEjTjMsCocYW59dEG19U3fwTTXxzi2Ed3yjPBp727hbbj53pHFw==
dependencies:
chokidar ">=3.0.0 <4.0.0"
immutable "^4.0.0"
@@ -9540,7 +9384,7 @@ serve-static@1.14.1:
parseurl "~1.3.3"
send "0.17.1"
-set-blocking@^2.0.0, set-blocking@~2.0.0:
+set-blocking@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
@@ -9638,16 +9482,16 @@ simple-html-tokenizer@^0.5.11:
resolved "https://registry.yarnpkg.com/simple-html-tokenizer/-/simple-html-tokenizer-0.5.11.tgz#4c5186083c164ba22a7b477b7687ac056ad6b1d9"
integrity sha512-C2WEK/Z3HoSFbYq8tI7ni3eOo/NneSPRoPpcM7WdLjFOArFuyXEjAoCdOC3DgMfRyziZQ1hCNR4mrNdWEvD0og==
-sinon@^14.0.1:
- version "14.0.1"
- resolved "https://registry.yarnpkg.com/sinon/-/sinon-14.0.1.tgz#9f02e13ad86b695c0c554525e3bf7f8245b31a9c"
- integrity sha512-JhJ0jCiyBWVAHDS+YSjgEbDn7Wgz9iIjA1/RK+eseJN0vAAWIWiXBdrnb92ELPyjsfreCYntD1ORtLSfIrlvSQ==
+sinon@^14.0.2:
+ version "14.0.2"
+ resolved "https://registry.yarnpkg.com/sinon/-/sinon-14.0.2.tgz#585a81a3c7b22cf950762ac4e7c28eb8b151c46f"
+ integrity sha512-PDpV0ZI3ZCS3pEqx0vpNp6kzPhHrLx72wA0G+ZLaaJjLIYeE0n8INlgaohKuGy7hP0as5tbUd23QWu5U233t+w==
dependencies:
- "@sinonjs/commons" "^1.8.3"
+ "@sinonjs/commons" "^2.0.0"
"@sinonjs/fake-timers" "^9.1.2"
- "@sinonjs/samsam" "^6.1.1"
+ "@sinonjs/samsam" "^7.0.1"
diff "^5.0.0"
- nise "^5.1.1"
+ nise "^5.1.2"
supports-color "^7.2.0"
slash@^1.0.0:
@@ -9690,57 +9534,30 @@ snapdragon@^0.8.1:
source-map-resolve "^0.5.0"
use "^3.1.0"
-socket.io-adapter@~1.1.0:
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-1.1.2.tgz#ab3f0d6f66b8fc7fca3959ab5991f82221789be9"
- integrity sha512-WzZRUj1kUjrTIrUKpZLEzFZ1OLj5FwLlAFQs9kuZJzJi5DKdU7FsWc36SNmA8iDOtwBQyT8FkrriRM8vXLYz8g==
-
-socket.io-client@2.4.0:
+socket.io-adapter@~2.4.0:
version "2.4.0"
- resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-2.4.0.tgz#aafb5d594a3c55a34355562fc8aea22ed9119a35"
- integrity sha512-M6xhnKQHuuZd4Ba9vltCLT9oa+YvTsP8j9NcEiLElfIg8KeYPyhWOes6x4t+LTAC8enQbE/995AdTem2uNyKKQ==
- dependencies:
- backo2 "1.0.2"
- component-bind "1.0.0"
- component-emitter "~1.3.0"
- debug "~3.1.0"
- engine.io-client "~3.5.0"
- has-binary2 "~1.0.2"
- indexof "0.0.1"
- parseqs "0.0.6"
- parseuri "0.0.6"
- socket.io-parser "~3.3.0"
- to-array "0.1.4"
+ resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.4.0.tgz#b50a4a9ecdd00c34d4c8c808224daa1a786152a6"
+ integrity sha512-W4N+o69rkMEGVuk2D/cvca3uYsvGlMwsySWV447y99gUPghxq42BxqLNMndb+a1mm/5/7NeXVQS7RLa2XyXvYg==
-socket.io-parser@~3.3.0:
- version "3.3.2"
- resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.3.2.tgz#ef872009d0adcf704f2fbe830191a14752ad50b6"
- integrity sha512-FJvDBuOALxdCI9qwRrO/Rfp9yfndRtc1jSgVgV8FDraihmSP/MLGD5PEuJrNfjALvcQ+vMDM/33AWOYP/JSjDg==
+socket.io-parser@~4.2.0:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.1.tgz#01c96efa11ded938dcb21cbe590c26af5eff65e5"
+ integrity sha512-V4GrkLy+HeF1F/en3SpUaM+7XxYXpuMUWLGde1kSSh5nQMN4hLrbPIkD+otwh6q9R6NOQBN4AMaOZ2zVjui82g==
dependencies:
- component-emitter "~1.3.0"
- debug "~3.1.0"
- isarray "2.0.1"
+ "@socket.io/component-emitter" "~3.1.0"
+ debug "~4.3.1"
-socket.io-parser@~3.4.0:
- version "3.4.1"
- resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.4.1.tgz#b06af838302975837eab2dc980037da24054d64a"
- integrity sha512-11hMgzL+WCLWf1uFtHSNvliI++tcRUWdoeYuwIl+Axvwy9z2gQM+7nJyN3STj1tLj5JyIUH8/gpDGxzAlDdi0A==
+socket.io@^4.1.2:
+ version "4.5.3"
+ resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-4.5.3.tgz#44dffea48d7f5aa41df4a66377c386b953bc521c"
+ integrity sha512-zdpnnKU+H6mOp7nYRXH4GNv1ux6HL6+lHL8g7Ds7Lj8CkdK1jJK/dlwsKDculbyOHifcJ0Pr/yeXnZQ5GeFrcg==
dependencies:
- component-emitter "1.2.1"
- debug "~4.1.0"
- isarray "2.0.1"
-
-socket.io@^2.1.0:
- version "2.4.1"
- resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-2.4.1.tgz#95ad861c9a52369d7f1a68acf0d4a1b16da451d2"
- integrity sha512-Si18v0mMXGAqLqCVpTxBa8MGqriHGQh8ccEOhmsmNS3thNCGBwO8WGrwMibANsWtQQ5NStdZwHqZR3naJVFc3w==
- dependencies:
- debug "~4.1.0"
- engine.io "~3.5.0"
- has-binary2 "~1.0.2"
- socket.io-adapter "~1.1.0"
- socket.io-client "2.4.0"
- socket.io-parser "~3.4.0"
+ accepts "~1.3.4"
+ base64id "~2.0.0"
+ debug "~4.3.2"
+ engine.io "~6.2.0"
+ socket.io-adapter "~2.4.0"
+ socket.io-parser "~4.2.0"
sort-object-keys@^1.1.3:
version "1.1.3"
@@ -9940,24 +9757,7 @@ string-template@~0.2.0, string-template@~0.2.1:
resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add"
integrity sha1-QpMuWYo1LQH8IuwzZ9nYTuxsmt0=
-string-width@^1.0.1:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
- integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=
- dependencies:
- code-point-at "^1.0.0"
- is-fullwidth-code-point "^1.0.0"
- strip-ansi "^3.0.0"
-
-"string-width@^1.0.2 || 2", string-width@^2.1.0:
- version "2.1.1"
- resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
- integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==
- dependencies:
- is-fullwidth-code-point "^2.0.0"
- strip-ansi "^4.0.0"
-
-"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.2.3:
+"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -9966,14 +9766,13 @@ string-width@^1.0.1:
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
-string-width@^4.1.0, string-width@^4.2.0:
- version "4.2.2"
- resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5"
- integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==
+string-width@^2.1.0:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
+ integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==
dependencies:
- emoji-regex "^8.0.0"
- is-fullwidth-code-point "^3.0.0"
- strip-ansi "^6.0.0"
+ is-fullwidth-code-point "^2.0.0"
+ strip-ansi "^4.0.0"
string.prototype.matchall@^4.0.5:
version "4.0.6"
@@ -10024,7 +9823,7 @@ string_decoder@~1.1.1:
dependencies:
safe-buffer "~5.1.0"
-strip-ansi@^3.0.0, strip-ansi@^3.0.1:
+strip-ansi@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=
@@ -10227,16 +10026,17 @@ terser@^5.3.0, terser@^5.7.2:
source-map-support "~0.5.20"
testem@^3.2.0:
- version "3.4.0"
- resolved "https://registry.yarnpkg.com/testem/-/testem-3.4.0.tgz#48ab6b98e96085eeddac1fb46337872b13e9e06c"
- integrity sha512-09mhy7fQj9o1W1c/Lfcs56FYqhFiZrXZjnOSJn+KxWAdYjbF5yHEuGrg+L5ooBlleCGD9r1TQwKd3+DixskT0Q==
+ version "3.9.0"
+ resolved "https://registry.yarnpkg.com/testem/-/testem-3.9.0.tgz#a82ccf01e5a248e3924244186e348c665ab90f7d"
+ integrity sha512-YTxCYKj0cc8uUSKEziJtSC5T/pw4fQnY0ZXNOyvAFgrijfsN9NxmncJZOHLhPgFOuhbRd5i+DBQxw0Cpe0SEFg==
dependencies:
+ "@xmldom/xmldom" "^0.8.0"
backbone "^1.1.2"
bluebird "^3.4.6"
charm "^1.0.0"
commander "^2.6.0"
compression "^1.7.4"
- consolidate "^0.15.1"
+ consolidate "^0.16.0"
execa "^1.0.0"
express "^4.10.7"
fireworm "^0.7.0"
@@ -10248,18 +10048,17 @@ testem@^3.2.0:
lodash.clonedeep "^4.4.1"
lodash.find "^4.5.1"
lodash.uniqby "^4.7.0"
- mkdirp "^0.5.1"
- mustache "^3.0.0"
- node-notifier "^9.0.1"
- npmlog "^4.0.0"
+ mkdirp "^1.0.4"
+ mustache "^4.2.0"
+ node-notifier "^10.0.0"
+ npmlog "^6.0.0"
printf "^0.6.1"
- rimraf "^2.4.4"
- socket.io "^2.1.0"
+ rimraf "^3.0.2"
+ socket.io "^4.1.2"
spawn-args "^0.2.0"
styled_string "0.0.1"
tap-parser "^7.0.0"
tmp "0.0.33"
- xmldom "^0.1.19"
text-table@^0.2.0:
version "0.2.0"
@@ -10348,14 +10147,9 @@ tmp@^0.1.0:
rimraf "^2.6.3"
tmpl@1.0.x:
- version "1.0.4"
- resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1"
- integrity sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=
-
-to-array@0.1.4:
- version "0.1.4"
- resolved "https://registry.yarnpkg.com/to-array/-/to-array-0.1.4.tgz#17e6c11f73dd4f3d74cda7a4ff3238e9ad9bf890"
- integrity sha1-F+bBH3PdTz10zaek/zI46a2b+JA=
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc"
+ integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==
to-arraybuffer@^1.0.0:
version "1.0.1"
@@ -10729,7 +10523,7 @@ utils-merge@1.0.1:
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
-uuid@^8.3.0, uuid@^8.3.2:
+uuid@^8.3.2:
version "8.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
@@ -10749,7 +10543,7 @@ validate-peer-dependencies@^1.2.0:
resolve-package-path "^3.1.0"
semver "^7.3.2"
-vary@~1.1.2:
+vary@^1, vary@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
@@ -11029,13 +10823,6 @@ which@^2.0.1, which@^2.0.2:
dependencies:
isexe "^2.0.0"
-wide-align@^1.1.0:
- version "1.1.3"
- resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457"
- integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==
- dependencies:
- string-width "^1.0.2 || 2"
-
wide-align@^1.1.5:
version "1.1.5"
resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3"
@@ -11104,10 +10891,10 @@ ws@^8.9.0:
resolved "https://registry.yarnpkg.com/ws/-/ws-8.9.0.tgz#2a994bb67144be1b53fe2d23c53c028adeb7f45e"
integrity sha512-Ja7nszREasGaYUYCI2k4lCKIRTt+y7XuqVoHR44YpI49TtryyqbqvDMn5eqfW7e6HzTukDRIsXqzVHScqRcafg==
-ws@~7.4.2:
- version "7.4.4"
- resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.4.tgz#383bc9742cb202292c9077ceab6f6047b17f2d59"
- integrity sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==
+ws@~8.2.3:
+ version "8.2.3"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba"
+ integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==
x-is-array@0.1.0:
version "0.1.0"
@@ -11134,16 +10921,6 @@ xmlchars@^2.2.0:
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
-xmldom@^0.1.19:
- version "0.1.31"
- resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.31.tgz#b76c9a1bd9f0a9737e5a72dc37231cf38375e2ff"
- integrity sha512-yS2uJflVQs6n+CyjHoaBmVSqIDevTAWrzMmjG1Gc7h1qQ7uVozNhEPJAwZXWyGQ/Gafo3fCwrcaokezLPupVyQ==
-
-xmlhttprequest-ssl@~1.5.4:
- version "1.5.5"
- resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz#c2876b06168aadc40e57d97e81191ac8f4398b3e"
- integrity sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=
-
xss@^1.0.14:
version "1.0.14"
resolved "https://registry.yarnpkg.com/xss/-/xss-1.0.14.tgz#4f3efbde75ad0d82e9921cc3c95e6590dd336694"
@@ -11180,11 +10957,6 @@ yam@^1.0.0:
fs-extra "^4.0.2"
lodash.merge "^4.6.0"
-yeast@0.1.2:
- version "0.1.2"
- resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419"
- integrity sha1-AI4G2AlDIMNy28L47XagymyKxBk=
-
yocto-queue@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
diff --git a/app/assets/stylesheets/common/base/_index.scss b/app/assets/stylesheets/common/base/_index.scss
index 163ec74b3c..eb810f8194 100644
--- a/app/assets/stylesheets/common/base/_index.scss
+++ b/app/assets/stylesheets/common/base/_index.scss
@@ -12,7 +12,6 @@
@import "crawler_layout";
@import "d-icon";
@import "d-popover";
-@import "d-onboarding";
@import "dialog";
@import "directory";
@import "discourse";
@@ -59,4 +58,5 @@
@import "topic";
@import "upload";
@import "user-badges";
+@import "user-tips";
@import "user";
diff --git a/app/assets/stylesheets/common/base/d-onboarding.scss b/app/assets/stylesheets/common/base/d-onboarding.scss
deleted file mode 100644
index f7f795d9d7..0000000000
--- a/app/assets/stylesheets/common/base/d-onboarding.scss
+++ /dev/null
@@ -1,37 +0,0 @@
-.onboarding-popup-container {
- min-width: 300px;
- padding: 0.5em;
- text-align: left;
-
- .onboarding-popup-title {
- font-size: $font-up-2;
- font-weight: bold;
- }
-
- .onboarding-popup-content {
- margin-top: 0.25em;
- }
-
- .onboarding-popup-buttons {
- margin-top: 1em;
- }
-}
-
-.tippy-box[data-theme~="d-onboarding"][data-placement^="left"]
- > .tippy-svg-arrow
- > svg {
- left: 11px;
-}
-
-.tippy-box[data-theme~="d-onboarding"][data-placement^="bottom"]
- > .tippy-svg-arrow
- > svg {
- top: -13px;
- left: -1px;
-}
-
-.tippy-box[data-theme~="d-onboarding"] > .tippy-svg-arrow:after,
-.tippy-box[data-theme~="d-onboarding"] > .tippy-svg-arrow > svg {
- width: 18px;
- height: 18px;
-}
diff --git a/app/assets/stylesheets/common/base/sidebar-section-link.scss b/app/assets/stylesheets/common/base/sidebar-section-link.scss
index b885a50bee..79a5043c71 100644
--- a/app/assets/stylesheets/common/base/sidebar-section-link.scss
+++ b/app/assets/stylesheets/common/base/sidebar-section-link.scss
@@ -36,7 +36,7 @@
@include ellipsis;
padding-left: 0.5em;
text-align: right;
- color: var(--tertiary);
+ color: var(--primary-high);
font-size: var(--font-down-1);
font-weight: normal;
margin-left: auto;
@@ -45,6 +45,7 @@
.sidebar-section-link-suffix {
margin-left: 0.25rem;
font-size: var(--font-down-4);
+ color: var(--tertiary-medium);
}
.sidebar-section-link-content-text {
diff --git a/app/assets/stylesheets/common/base/user-tips.scss b/app/assets/stylesheets/common/base/user-tips.scss
new file mode 100644
index 0000000000..a20bc51cce
--- /dev/null
+++ b/app/assets/stylesheets/common/base/user-tips.scss
@@ -0,0 +1,37 @@
+.user-tip-container {
+ min-width: 300px;
+ padding: 0.5em;
+ text-align: left;
+
+ .user-tip-title {
+ font-size: $font-up-2;
+ font-weight: bold;
+ }
+
+ .user-tip-content {
+ margin-top: 0.25em;
+ }
+
+ .user-tip-buttons {
+ margin-top: 1em;
+ }
+}
+
+.tippy-box[data-theme~="user-tips"][data-placement^="left"]
+ > .tippy-svg-arrow
+ > svg {
+ left: 11px;
+}
+
+.tippy-box[data-theme~="user-tips"][data-placement^="bottom"]
+ > .tippy-svg-arrow
+ > svg {
+ top: -13px;
+ left: -1px;
+}
+
+.tippy-box[data-theme~="user-tips"] > .tippy-svg-arrow:after,
+.tippy-box[data-theme~="user-tips"] > .tippy-svg-arrow > svg {
+ width: 18px;
+ height: 18px;
+}
diff --git a/app/assets/stylesheets/desktop/topic-list.scss b/app/assets/stylesheets/desktop/topic-list.scss
index e91f197542..1c274156ae 100644
--- a/app/assets/stylesheets/desktop/topic-list.scss
+++ b/app/assets/stylesheets/desktop/topic-list.scss
@@ -269,9 +269,6 @@
display: flex;
flex-direction: row;
justify-content: space-between;
- position: absolute;
- top: 48px;
- width: calc(100% - 40px);
z-index: z("usercard");
&__content {
width: 70%;
diff --git a/app/assets/stylesheets/mobile/topic-post.scss b/app/assets/stylesheets/mobile/topic-post.scss
index e5ff07b7da..dd4e81e0e4 100644
--- a/app/assets/stylesheets/mobile/topic-post.scss
+++ b/app/assets/stylesheets/mobile/topic-post.scss
@@ -342,6 +342,18 @@ span.highlighted {
/* must render on top of topic-body + topic-meta-data, otherwise not tappable */
}
+.small-action .topic-avatar {
+ display: flex;
+ align-self: stretch;
+ align-items: flex-start;
+ margin-right: 0;
+ float: unset;
+ height: auto;
+ .d-icon {
+ font-size: 1.8em;
+ }
+}
+
.topic-meta-data {
margin-left: 50px;
font-size: var(--font-down-1);
diff --git a/app/assets/stylesheets/publish.scss b/app/assets/stylesheets/publish.scss
index cdd768b751..2055ea405b 100644
--- a/app/assets/stylesheets/publish.scss
+++ b/app/assets/stylesheets/publish.scss
@@ -1,11 +1,18 @@
@import "common";
.published-page-content-wrapper {
+ box-sizing: border-box;
margin: 2em auto;
- max-width: 800px;
+ padding: 0.5em 10px 2em; // 10px matches .wrap
+ max-width: calc(
+ var(--topic-body-width) + (var(--topic-body-width-padding) * 2) +
+ var(--topic-avatar-width)
+ );
+ width: 100%;
}
.published-page-header {
+ box-sizing: border-box;
width: 100%;
top: 0;
z-index: z("header");
@@ -14,10 +21,15 @@
position: sticky;
top: 0;
.published-page-header-wrapper {
- width: 925px;
+ box-sizing: border-box;
+ max-width: calc(
+ var(--topic-body-width) + (var(--topic-body-width-padding) * 2) +
+ var(--topic-avatar-width)
+ );
+ width: 100%;
display: flex;
margin: 0em auto;
- padding: 0.5em 0;
+ padding: 0.5em 10px; // 10px matches .wrap padding
align-items: center;
.published-page-logo {
height: 45px;
@@ -26,9 +38,12 @@
}
.published-page-title {
color: var(--header_primary);
- font-size: 2em;
+ font-size: var(--font-up-5);
margin: 0;
- max-height: 1.25em;
+ line-height: var(--line-height-medium);
+ @include breakpoint(mobile-extra-large) {
+ font-size: var(--font-up-3);
+ }
}
}
}
@@ -62,5 +77,8 @@
.published-page-content-body {
font-size: 1.25em;
- padding-bottom: 2em;
+ img {
+ max-width: 100%;
+ height: auto;
+ }
}
diff --git a/app/assets/stylesheets/publish_desktop.scss b/app/assets/stylesheets/publish_desktop.scss
deleted file mode 100644
index d9611b081d..0000000000
--- a/app/assets/stylesheets/publish_desktop.scss
+++ /dev/null
@@ -1 +0,0 @@
-@import "publish";
diff --git a/app/assets/stylesheets/publish_desktop_rtl.scss b/app/assets/stylesheets/publish_desktop_rtl.scss
deleted file mode 100644
index b13ca0ae62..0000000000
--- a/app/assets/stylesheets/publish_desktop_rtl.scss
+++ /dev/null
@@ -1 +0,0 @@
-@import "publish_desktop";
diff --git a/app/assets/stylesheets/publish_mobile.scss b/app/assets/stylesheets/publish_mobile.scss
deleted file mode 100644
index cdd7d9259c..0000000000
--- a/app/assets/stylesheets/publish_mobile.scss
+++ /dev/null
@@ -1,20 +0,0 @@
-@import "publish";
-
-.published-page-header {
- .published-page-header-wrapper {
- width: auto;
-
- .published-page-title {
- font-size: var(--font-up-3);
- width: 100%;
- overflow: hidden;
- white-space: nowrap;
- text-overflow: ellipsis;
- }
- }
-}
-
-.published-page-content-wrapper {
- margin: 2em auto;
- padding: 0 10px;
-}
diff --git a/app/assets/stylesheets/publish_mobile_rtl.scss b/app/assets/stylesheets/publish_mobile_rtl.scss
deleted file mode 100644
index 0b2e7dfe0b..0000000000
--- a/app/assets/stylesheets/publish_mobile_rtl.scss
+++ /dev/null
@@ -1 +0,0 @@
-@import "publish_mobile";
diff --git a/app/assets/stylesheets/testem.scss b/app/assets/stylesheets/testem.scss
index cb117a8825..a1e4c5dad3 100644
--- a/app/assets/stylesheets/testem.scss
+++ b/app/assets/stylesheets/testem.scss
@@ -19,6 +19,7 @@ $love: #fa6c8d !default;
@import "common/foundation/mixins";
@import "desktop";
@import "color_definitions";
+@import "admin";
#ember-testing-container {
box-sizing: border-box;
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index 377ceccaac..788f5e6006 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -111,7 +111,7 @@ class Admin::GroupsController < Admin::StaffController
raise Discourse::NotFound unless group
users = User.where(username: group_params[:usernames].split(","))
- users.each { |user| guardian.ensure_can_change_primary_group!(user) }
+ users.each { |user| guardian.ensure_can_change_primary_group!(user, group) }
users.update_all(primary_group_id: params[:primary] == "true" ? group.id : nil)
render json: success_json
diff --git a/app/controllers/admin/site_settings_controller.rb b/app/controllers/admin/site_settings_controller.rb
index a0a6ad9b86..4db8b72e43 100644
--- a/app/controllers/admin/site_settings_controller.rb
+++ b/app/controllers/admin/site_settings_controller.rb
@@ -197,7 +197,8 @@ class Admin::SiteSettingsController < Admin::AdminController
default_email_digest_frequency: "digest_after_minutes",
default_include_tl0_in_digests: "include_tl0_in_digests",
default_text_size: "text_size_key",
- default_title_count_mode: "title_count_mode_key"
+ default_title_count_mode: "title_count_mode_key",
+ default_hide_profile_and_presence: "hide_profile_and_presence"
}
end
diff --git a/app/controllers/admin/themes_controller.rb b/app/controllers/admin/themes_controller.rb
index 7f3013e65f..a47f2016d7 100644
--- a/app/controllers/admin/themes_controller.rb
+++ b/app/controllers/admin/themes_controller.rb
@@ -97,32 +97,32 @@ class Admin::ThemesController < Admin::AdminController
return
end
- begin
- branch = params[:branch] ? params[:branch] : nil
- private_key = params[:public_key] ? Discourse.redis.get("ssh_key_#{params[:public_key]}") : nil
- return render_json_error I18n.t("themes.import_error.ssh_key_gone") if params[:public_key].present? && private_key.blank?
+ hijack do
+ begin
+ branch = params[:branch] ? params[:branch] : nil
+ private_key = params[:public_key] ? Discourse.redis.get("ssh_key_#{params[:public_key]}") : nil
+ return render_json_error I18n.t("themes.import_error.ssh_key_gone") if params[:public_key].present? && private_key.blank?
- hijack do
@theme = RemoteTheme.import_theme(remote, theme_user, private_key: private_key, branch: branch)
render json: @theme, status: :created
- end
- rescue RemoteTheme::ImportError => e
- if params[:force]
- theme_name = params[:remote].gsub(/.git$/, "").split("/").last
+ rescue RemoteTheme::ImportError => e
+ if params[:force]
+ theme_name = params[:remote].gsub(/.git$/, "").split("/").last
- remote_theme = RemoteTheme.new
- remote_theme.private_key = private_key
- remote_theme.branch = params[:branch] ? params[:branch] : nil
- remote_theme.remote_url = params[:remote]
- remote_theme.save!
+ remote_theme = RemoteTheme.new
+ remote_theme.private_key = private_key
+ remote_theme.branch = params[:branch] ? params[:branch] : nil
+ remote_theme.remote_url = params[:remote]
+ remote_theme.save!
- @theme = Theme.new(user_id: theme_user&.id || -1, name: theme_name)
- @theme.remote_theme = remote_theme
- @theme.save!
+ @theme = Theme.new(user_id: theme_user&.id || -1, name: theme_name)
+ @theme.remote_theme = remote_theme
+ @theme.save!
- render json: @theme, status: :created
- else
- render_json_error e.message
+ render json: @theme, status: :created
+ else
+ render_json_error e.message
+ end
end
end
elsif params[:bundle] || (params[:theme] && THEME_CONTENT_TYPES.include?(params[:theme].content_type))
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index 86a9ad4df1..6d2cf1d3fd 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -241,11 +241,11 @@ class Admin::UsersController < Admin::StaffController
end
def primary_group
- guardian.ensure_can_change_primary_group!(@user)
-
if params[:primary_group_id].present?
primary_group_id = params[:primary_group_id].to_i
if group = Group.find(primary_group_id)
+ guardian.ensure_can_change_primary_group!(@user, group)
+
if group.user_ids.include?(@user.id)
@user.primary_group_id = primary_group_id
end
diff --git a/app/controllers/categories_controller.rb b/app/controllers/categories_controller.rb
index 33325cf61c..d6e1a6e2de 100644
--- a/app/controllers/categories_controller.rb
+++ b/app/controllers/categories_controller.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
class CategoriesController < ApplicationController
+ include TopicQueryParams
requires_login except: [:index, :categories_and_latest, :categories_and_top, :show, :redirect, :find_by_slug, :visible_groups]
@@ -291,6 +292,8 @@ class CategoriesController < ApplicationController
per_page: CategoriesController.topics_per_page,
no_definitions: true,
}
+
+ topic_options.merge!(build_topic_list_options)
style = SiteSetting.desktop_category_page_style
topic_options[:order] = 'created' if style == "categories_and_latest_topics_created_date"
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 0bc45d0ddf..d4b856d73b 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -65,7 +65,7 @@ class GroupsController < ApplicationController
if !guardian.is_staff?
# hide automatic groups from all non stuff to de-clutter page
- groups = groups.where("automatic IS FALSE OR groups.id = #{Group::AUTO_GROUPS[:moderators]}")
+ groups = groups.where("automatic IS FALSE OR groups.id = ?", Group::AUTO_GROUPS[:moderators])
type_filters.delete(:automatic)
end
@@ -129,7 +129,7 @@ class GroupsController < ApplicationController
format.json do
groups = Group.visible_groups(current_user)
if !guardian.is_staff?
- groups = groups.where("automatic IS FALSE OR groups.id = #{Group::AUTO_GROUPS[:moderators]}")
+ groups = groups.where("automatic IS FALSE OR groups.id = ?", Group::AUTO_GROUPS[:moderators])
end
render_json_dump(
diff --git a/app/controllers/new_topic_controller.rb b/app/controllers/new_topic_controller.rb
new file mode 100644
index 0000000000..f6b9017666
--- /dev/null
+++ b/app/controllers/new_topic_controller.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class NewTopicController < ApplicationController
+ def index; end
+end
diff --git a/app/controllers/session_controller.rb b/app/controllers/session_controller.rb
index d05ba4351f..490f4db0ed 100644
--- a/app/controllers/session_controller.rb
+++ b/app/controllers/session_controller.rb
@@ -760,7 +760,7 @@ class SessionController < ApplicationController
end
if invite.redeemable?
- if !invite.is_invite_link? && sso.email != invite.email
+ if invite.is_email_invite? && sso.email != invite.email
raise Invite::ValidationFailed.new(I18n.t("invite.not_matching_email"))
end
elsif invite.expired?
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index d2078d15eb..96d3c11a65 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -626,13 +626,16 @@ module ApplicationHelper
end
def discourse_theme_color_meta_tags
- result = +<<~HTML
-
- HTML
+ result = +""
if dark_scheme_id != -1
result << <<~HTML
+
HTML
+ else
+ result << <<~HTML
+
+ HTML
end
result.html_safe
end
diff --git a/app/helpers/email_helper.rb b/app/helpers/email_helper.rb
index 952fb40cfe..ba14776aff 100644
--- a/app/helpers/email_helper.rb
+++ b/app/helpers/email_helper.rb
@@ -60,7 +60,7 @@ module EmailHelper
p,
span,
td {
- color: #dddddd !important;
+ color: inherit !important;
}
[data-stripped-secure-media] {
diff --git a/app/jobs/onceoff/migrate_custom_emojis.rb b/app/jobs/onceoff/migrate_custom_emojis.rb
index 866ec4e421..f359a23c99 100644
--- a/app/jobs/onceoff/migrate_custom_emojis.rb
+++ b/app/jobs/onceoff/migrate_custom_emojis.rb
@@ -29,7 +29,7 @@ module Jobs
Emoji.clear_cache
- Post.where("cooked LIKE '%#{Emoji.base_url}%'").find_each do |post|
+ Post.where("cooked LIKE ?", "%#{Emoji.base_url}%").find_each do |post|
post.rebake!
end
end
diff --git a/app/jobs/regular/anonymize_user.rb b/app/jobs/regular/anonymize_user.rb
index c078706c04..7343163e49 100644
--- a/app/jobs/regular/anonymize_user.rb
+++ b/app/jobs/regular/anonymize_user.rb
@@ -46,8 +46,9 @@ module Jobs
# UserHistory for delete_user logs the user's IP. Note this is quite ugly but we don't
# have a better way of querying on details right now.
UserHistory.where(
- "action = :action AND details LIKE 'id: #{@user_id}\n%'",
- action: UserHistory.actions[:delete_user]
+ "action = :action AND details LIKE :details",
+ action: UserHistory.actions[:delete_user],
+ details: "id: #{@user_id}\n%",
).update_all(ip_address: new_ip)
end
diff --git a/app/jobs/regular/export_user_archive.rb b/app/jobs/regular/export_user_archive.rb
index 256b8a3d4d..657cc4aafe 100644
--- a/app/jobs/regular/export_user_archive.rb
+++ b/app/jobs/regular/export_user_archive.rb
@@ -26,7 +26,7 @@ module Jobs
)
HEADER_ATTRS_FOR ||= HashWithIndifferentAccess.new(
- user_archive: ['topic_title', 'categories', 'is_pm', 'post', 'like_count', 'reply_count', 'url', 'created_at'],
+ user_archive: ['topic_title', 'categories', 'is_pm', 'post_raw', 'post_cooked', 'like_count', 'reply_count', 'url', 'created_at'],
user_archive_profile: ['location', 'website', 'bio', 'views'],
auth_tokens: ['id', 'auth_token_hash', 'prev_auth_token_hash', 'auth_token_seen', 'client_ip', 'user_agent', 'seen_at', 'rotated_at', 'created_at', 'updated_at'],
auth_token_logs: ['id', 'action', 'user_auth_token_id', 'client_ip', 'auth_token_hash', 'created_at', 'path', 'user_agent'],
@@ -134,7 +134,7 @@ module Jobs
Post.includes(topic: :category)
.where(user_id: @current_user.id)
- .select(:topic_id, :post_number, :raw, :like_count, :reply_count, :created_at)
+ .select(:topic_id, :post_number, :raw, :cooked, :like_count, :reply_count, :created_at)
.order(:created_at)
.with_deleted
.each do |user_archive|
@@ -441,7 +441,15 @@ module Jobs
is_pm = topic_data.archetype == "private_message" ? I18n.t("csv_export.boolean_yes") : I18n.t("csv_export.boolean_no")
url = "#{Discourse.base_url}/t/#{topic_data.slug}/#{topic_data.id}/#{user_archive['post_number']}"
- topic_hash = { "post" => user_archive['raw'], "topic_title" => topic_data.title, "categories" => categories, "is_pm" => is_pm, "url" => url }
+ topic_hash = {
+ "post_raw" => user_archive['raw'],
+ "post_cooked" => user_archive["cooked"],
+ "topic_title" => topic_data.title,
+ "categories" => categories,
+ "is_pm" => is_pm,
+ "url" => url
+ }
+
user_archive.merge!(topic_hash)
HEADER_ATTRS_FOR['user_archive'].each do |attr|
diff --git a/app/jobs/scheduled/clean_up_uploads.rb b/app/jobs/scheduled/clean_up_uploads.rb
index 9c9ce5df3c..ecaa5d3876 100644
--- a/app/jobs/scheduled/clean_up_uploads.rb
+++ b/app/jobs/scheduled/clean_up_uploads.rb
@@ -41,9 +41,9 @@ module Jobs
if upload.sha1.present?
# TODO: Remove this check after UploadReferences records were created
encoded_sha = Base62.encode(upload.sha1.hex)
- next if ReviewableQueuedPost.pending.where("payload->>'raw' LIKE '%#{upload.sha1}%' OR payload->>'raw' LIKE '%#{encoded_sha}%'").exists?
- next if Draft.where("data LIKE '%#{upload.sha1}%' OR data LIKE '%#{encoded_sha}%'").exists?
- next if UserProfile.where("bio_raw LIKE '%#{upload.sha1}%' OR bio_raw LIKE '%#{encoded_sha}%'").exists?
+ next if ReviewableQueuedPost.pending.where("payload->>'raw' LIKE ? OR payload->>'raw' LIKE ?", "%#{upload.sha1}%", "%#{encoded_sha}%").exists?
+ next if Draft.where("data LIKE ? OR data LIKE ?", "%#{upload.sha1}%", "%#{encoded_sha}%").exists?
+ next if UserProfile.where("bio_raw LIKE ? OR bio_raw LIKE ?", "%#{upload.sha1}%", "%#{encoded_sha}%").exists?
upload.destroy
else
diff --git a/app/jobs/scheduled/enqueue_digest_emails.rb b/app/jobs/scheduled/enqueue_digest_emails.rb
index e76a60d97e..7d9ad9fa16 100644
--- a/app/jobs/scheduled/enqueue_digest_emails.rb
+++ b/app/jobs/scheduled/enqueue_digest_emails.rb
@@ -23,12 +23,12 @@ module Jobs
.where(staged: false)
.joins(:user_option, :user_stat, :user_emails)
.where("user_options.email_digests")
- .where("user_stats.bounce_score < #{SiteSetting.bounce_score_threshold}")
+ .where("user_stats.bounce_score < ?", SiteSetting.bounce_score_threshold)
.where("user_emails.primary")
.where("COALESCE(last_emailed_at, '2010-01-01') <= CURRENT_TIMESTAMP - ('1 MINUTE'::INTERVAL * user_options.digest_after_minutes)")
.where("COALESCE(user_stats.digest_attempted_at, '2010-01-01') <= CURRENT_TIMESTAMP - ('1 MINUTE'::INTERVAL * user_options.digest_after_minutes)")
.where("COALESCE(last_seen_at, '2010-01-01') <= CURRENT_TIMESTAMP - ('1 MINUTE'::INTERVAL * user_options.digest_after_minutes)")
- .where("COALESCE(last_seen_at, '2010-01-01') >= CURRENT_TIMESTAMP - ('1 DAY'::INTERVAL * #{SiteSetting.suppress_digest_email_after_days})")
+ .where("COALESCE(last_seen_at, '2010-01-01') >= CURRENT_TIMESTAMP - ('1 DAY'::INTERVAL * ?)", SiteSetting.suppress_digest_email_after_days)
.order("user_stats.digest_attempted_at ASC NULLS FIRST")
# If the site requires approval, make sure the user is approved
diff --git a/app/models/bookmark.rb b/app/models/bookmark.rb
index 2bd607d1e6..c6947cf078 100644
--- a/app/models/bookmark.rb
+++ b/app/models/bookmark.rb
@@ -204,10 +204,8 @@ end
#
# Indexes
#
-# idx_bookmarks_user_polymorphic_unique (user_id,bookmarkable_type,bookmarkable_id) UNIQUE
-# index_bookmarks_on_post_id (post_id)
-# index_bookmarks_on_reminder_at (reminder_at)
-# index_bookmarks_on_reminder_set_at (reminder_set_at)
-# index_bookmarks_on_user_id (user_id)
-# index_bookmarks_on_user_id_and_post_id_and_for_topic (user_id,post_id,for_topic) UNIQUE
+# idx_bookmarks_user_polymorphic_unique (user_id,bookmarkable_type,bookmarkable_id) UNIQUE
+# index_bookmarks_on_reminder_at (reminder_at)
+# index_bookmarks_on_reminder_set_at (reminder_set_at)
+# index_bookmarks_on_user_id (user_id)
#
diff --git a/app/models/category.rb b/app/models/category.rb
index 0efbb5f3ed..7e7bd947b0 100644
--- a/app/models/category.rb
+++ b/app/models/category.rb
@@ -374,7 +374,7 @@ class Category < ActiveRecord::Base
elsif SiteSetting.slug_generation_method == 'ascii' && !CGI.unescape(self.slug).ascii_only?
errors.add(:slug, I18n.t("category.errors.slug_contains_non_ascii_chars"))
elsif duplicate_slug?
- errors.add(:slug, 'is already in use')
+ errors.add(:slug, I18n.t("category.errors.is_already_in_use"))
end
else
# auto slug
diff --git a/app/models/concerns/has_custom_fields.rb b/app/models/concerns/has_custom_fields.rb
index 16d9872344..07ebc880ce 100644
--- a/app/models/concerns/has_custom_fields.rb
+++ b/app/models/concerns/has_custom_fields.rb
@@ -64,9 +64,7 @@ module HasCustomFields
has_many :_custom_fields, dependent: :destroy, class_name: "#{name}CustomField"
after_save :save_custom_fields
- # TODO (martin) Post 2.8 release, change to attr_reader because this is
- # set by set_preloaded_custom_fields
- attr_accessor :preloaded_custom_fields
+ attr_reader :preloaded_custom_fields
def custom_fields_fk
@custom_fields_fk ||= "#{_custom_fields.reflect_on_all_associations(:belongs_to)[0].name}_id"
diff --git a/app/models/concerns/second_factor_manager.rb b/app/models/concerns/second_factor_manager.rb
index e21957e75f..4e212306af 100644
--- a/app/models/concerns/second_factor_manager.rb
+++ b/app/models/concerns/second_factor_manager.rb
@@ -79,7 +79,7 @@ module SecondFactorManager
end
def has_any_second_factor_methods_enabled?
- totp_enabled? || security_keys_enabled?
+ totp_enabled? || security_keys_enabled? || backup_codes_enabled?
end
def has_multiple_second_factor_methods?
diff --git a/app/models/invite.rb b/app/models/invite.rb
index 917b486f29..1910700185 100644
--- a/app/models/invite.rb
+++ b/app/models/invite.rb
@@ -74,8 +74,16 @@ class Invite < ActiveRecord::Base
end
end
+ # Even if a domain is specified on the invite, it still counts as
+ # an invite link.
def is_invite_link?
- email.blank?
+ self.email.blank?
+ end
+
+ # Email invites have specific behaviour and it's easier to visually
+ # parse is_email_invite? than !is_invite_link?
+ def is_email_invite?
+ self.email.present?
end
def redeemable?
@@ -201,8 +209,6 @@ class Invite < ActiveRecord::Base
)
return if !redeemable?
- email = self.email if email.blank? && !is_invite_link?
-
InviteRedeemer.new(
invite: self,
email: email,
diff --git a/app/models/invite_redeemer.rb b/app/models/invite_redeemer.rb
index ba823aaf51..6a7ae4b22a 100644
--- a/app/models/invite_redeemer.rb
+++ b/app/models/invite_redeemer.rb
@@ -1,5 +1,18 @@
# frozen_string_literal: true
+# NOTE: There are a _lot_ of complicated rules and conditions for our
+# invite system, and the code is spread out through a lot of places.
+# Tread lightly and read carefully when modifying this code. You may
+# also want to look at:
+#
+# * InvitesController
+# * SessionController
+# * Invite model
+# * User model
+#
+# Invites that are scoped to a specific email (email IS NOT NULL on the Invite
+# model) have different rules to invites that are considered an "invite link",
+# (email IS NULL) on the Invite model.
class InviteRedeemer
attr_reader :invite,
:email,
@@ -13,7 +26,7 @@ class InviteRedeemer
:redeeming_user
def initialize(
- invite: nil,
+ invite:,
email: nil,
username: nil,
name: nil,
@@ -23,9 +36,7 @@ class InviteRedeemer
session: nil,
email_token: nil,
redeeming_user: nil)
-
@invite = invite
- @email = email
@username = username
@name = name
@password = password
@@ -34,6 +45,8 @@ class InviteRedeemer
@session = session
@email_token = email_token
@redeeming_user = redeeming_user
+
+ ensure_email_is_present!(email)
end
def redeem
@@ -45,7 +58,29 @@ class InviteRedeemer
end
end
- # extracted from User cause it is very specific to invites
+ # The email must be present in some form since many of the methods
+ # for processing + redemption rely on it. If it's still nil after
+ # these checks then we have hit an edge case and should not proceed!
+ def ensure_email_is_present!(email)
+ if email.blank?
+ Rails.logger.warn(
+ "email param was blank in InviteRedeemer for invite ID #{@invite.id}. The `redeeming_user` was #{@redeeming_user.present? ? "(ID: #{@redeeming_user.id})" : "not"} present.",
+ )
+ end
+
+ if email.blank? && @invite.is_email_invite?
+ @email = @invite.email
+ elsif @redeeming_user.present?
+ @email = @redeeming_user.email
+ else
+ @email = email
+ end
+
+ raise Discourse::InvalidParameters if @email.blank?
+ end
+
+ # This will _never_ be called if there is a redeeming_user being passed
+ # in to InviteRedeemer -- see invited_user below.
def self.create_user_from_invite(email:, invite:, username: nil, name: nil, password: nil, user_custom_fields: nil, ip_address: nil, session: nil, email_token: nil)
if username && UsernameValidator.new(username).valid_format? && User.username_available?(username, email)
available_username = username
@@ -107,7 +142,10 @@ class InviteRedeemer
user.save!
authenticator.finish
- if invite.emailed_status != Invite.emailed_status_types[:not_required] && email == invite.email && invite.email_token.present? && email_token == invite.email_token
+ if invite.emailed_status != Invite.emailed_status_types[:not_required] &&
+ email == invite.email &&
+ invite.email_token.present? &&
+ email_token == invite.email_token
user.activate
end
@@ -118,24 +156,26 @@ class InviteRedeemer
def can_redeem_invite?
return false if !invite.redeemable?
+ return false if email.blank?
- # Invite has already been redeemed by anyone.
- if !invite.is_invite_link? && InvitedUser.exists?(invite_id: invite.id)
+ # Invite scoped to email has already been redeemed by anyone.
+ if invite.is_email_invite? && InvitedUser.exists?(invite_id: invite.id)
return false
end
- # Email will not be present if we are claiming an invite link, which
- # does not have an email or domain scope on the invitation.
- if email.present? || redeeming_user.present?
- email_to_check = redeeming_user&.email || email
+ # The email will be present for either an invite link (where the user provides
+ # us the email manually) or for an invite scoped to an email, where we
+ # prefill the email and do not let the user modify it.
+ #
+ # Note that an invite link can also have a domain scope which must be checked.
+ email_to_check = redeeming_user&.email || email
- if invite.email.present? && !invite.email_matches?(email_to_check)
- raise ActiveRecord::RecordNotSaved.new(I18n.t('invite.not_matching_email'))
- end
+ if invite.email.present? && !invite.email_matches?(email_to_check)
+ raise ActiveRecord::RecordNotSaved.new(I18n.t('invite.not_matching_email'))
+ end
- if invite.domain.present? && !invite.domain_matches?(email_to_check)
- raise ActiveRecord::RecordNotSaved.new(I18n.t('invite.domain_not_allowed'))
- end
+ if invite.domain.present? && !invite.domain_matches?(email_to_check)
+ raise ActiveRecord::RecordNotSaved.new(I18n.t('invite.domain_not_allowed'))
end
# Anon user is trying to redeem an invitation, if an existing user already
@@ -148,6 +188,10 @@ class InviteRedeemer
true
end
+ # Note that the invited_user is returned by #redeemed, so other places
+ # (e.g. the InvitesController) can perform further actions on it, this
+ # is why things like send_welcome_message are set without being saved
+ # on the model.
def invited_user
return @invited_user if defined?(@invited_user)
@@ -196,9 +240,18 @@ class InviteRedeemer
end
def add_to_private_topics_if_invited
- topic_ids = Topic.where(archetype: Archetype::private_message).includes(:invites).where(invites: { email: email }).pluck(:id)
+ # Should not happen because of ensure_email_is_present!, but better to cover bases.
+ return if email.blank?
+
+ topic_ids = TopicInvite.joins(:invite)
+ .joins(:topic)
+ .where("topics.archetype = ?", Archetype::private_message)
+ .where("invites.email = ?", email)
+ .pluck(:topic_id)
topic_ids.each do |id|
- TopicAllowedUser.create!(user_id: invited_user.id, topic_id: id) unless TopicAllowedUser.exists?(user_id: invited_user.id, topic_id: id)
+ if !TopicAllowedUser.exists?(user_id: invited_user.id, topic_id: id)
+ TopicAllowedUser.create!(user_id: invited_user.id, topic_id: id)
+ end
end
end
@@ -209,6 +262,7 @@ class InviteRedeemer
group = Group.find_by(id: id)
if guardian.can_edit_group?(group)
invited_user.group_users.create!(group_id: group.id)
+ GroupActionLogger.new(invite.invited_by, group).log_add_user_to_group(invited_user)
DiscourseEvent.trigger(:user_added_to_group, invited_user, group, automatic: false)
end
end
@@ -220,15 +274,17 @@ class InviteRedeemer
end
def notify_invitee
- if inviter = invite.invited_by
- inviter.notifications.create!(
- notification_type: Notification.types[:invitee_accepted],
- data: { display_username: invited_user.username }.to_json
- )
- end
+ return if invite.invited_by.blank?
+ invite.invited_by.notifications.create!(
+ notification_type: Notification.types[:invitee_accepted],
+ data: { display_username: invited_user.username }.to_json
+ )
end
def delete_duplicate_invites
+ # Should not happen because of ensure_email_is_present!, but better to cover bases.
+ return if email.blank?
+
Invite
.where('invites.max_redemptions_allowed = 1')
.joins("LEFT JOIN invited_users ON invites.id = invited_users.invite_id")
diff --git a/app/models/onboarding_popup.rb b/app/models/onboarding_popup.rb
deleted file mode 100644
index f672f6eea4..0000000000
--- a/app/models/onboarding_popup.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# frozen_string_literal: true
-
-class OnboardingPopup
- def self.types
- @types ||= Enum.new(
- first_notification: 1,
- topic_timeline: 2,
- )
- end
-end
diff --git a/app/models/post.rb b/app/models/post.rb
index 5f493c4585..362ce9bf8b 100644
--- a/app/models/post.rb
+++ b/app/models/post.rb
@@ -989,10 +989,6 @@ class Post < ActiveRecord::Base
end
end
- def downloaded_images
- self.custom_fields[Post::DOWNLOADED_IMAGES] || {}
- end
-
def each_upload_url(fragments: nil, include_local_upload: true)
current_db = RailsMultisite::ConnectionManagement.current_db
upload_patterns = [
diff --git a/app/models/site_setting.rb b/app/models/site_setting.rb
index fc73aa76dc..7ec4016dc1 100644
--- a/app/models/site_setting.rb
+++ b/app/models/site_setting.rb
@@ -106,14 +106,6 @@ class SiteSetting < ActiveRecord::Base
SiteSetting.manual_polling_enabled? || SiteSetting.pop3_polling_enabled?
end
- WATCHED_SETTINGS ||= [
- :default_locale,
- :blocked_attachment_content_types,
- :blocked_attachment_filenames,
- :allowed_unicode_username_characters,
- :markdown_typographer_quotation_marks
- ]
-
def self.blocked_attachment_content_types_regex
current_db = RailsMultisite::ConnectionManagement.current_db
@@ -193,8 +185,7 @@ class SiteSetting < ActiveRecord::Base
def self.whispers_allowed_group_ids
if SiteSetting.enable_whispers && SiteSetting.whispers_allowed_groups.present?
- # TODO (martin) Change to whispers_allowed_groups_map
- SiteSetting.whispers_allowed_groups.split("|").map(&:to_i)
+ SiteSetting.whispers_allowed_groups_map
else
[]
end
diff --git a/app/models/upload.rb b/app/models/upload.rb
index 813c5e57e9..ad004f6e66 100644
--- a/app/models/upload.rb
+++ b/app/models/upload.rb
@@ -34,6 +34,7 @@ class Upload < ActiveRecord::Base
attr_accessor :for_export
attr_accessor :for_site_setting
attr_accessor :for_gravatar
+ attr_accessor :validate_file_size
validates_presence_of :filesize
validates_presence_of :original_filename
@@ -92,6 +93,11 @@ class Upload < ActiveRecord::Base
.where("ur.upload_id IS NULL")
end
+ def initialize(*args)
+ super
+ self.validate_file_size = true
+ end
+
def to_s
self.url
end
@@ -480,7 +486,7 @@ class Upload < ActiveRecord::Base
db = RailsMultisite::ConnectionManagement.current_db
scope = Upload.by_users
- .where("url NOT LIKE '%/original/_X/%' AND url LIKE '%/uploads/#{db}%'")
+ .where("url NOT LIKE '%/original/_X/%' AND url LIKE ?", "%/uploads/#{db}%")
.order(id: :desc)
scope = scope.limit(limit) if limit
diff --git a/app/models/user.rb b/app/models/user.rb
index d92780a13b..59a881e6f4 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -138,7 +138,7 @@ class User < ActiveRecord::Base
after_create :set_default_sidebar_section_links
after_update :set_default_sidebar_section_links, if: Proc.new {
- self.saved_change_to_staged?
+ self.saved_change_to_staged? || self.saved_change_to_admin?
}
after_update :trigger_user_updated_event, if: Proc.new {
@@ -284,6 +284,13 @@ class User < ActiveRecord::Base
MAX_STAFF_DELETE_POST_COUNT ||= 5
+ def self.user_tips
+ @user_tips ||= Enum.new(
+ first_notification: 1,
+ topic_timeline: 2,
+ )
+ end
+
def visible_sidebar_tags(user_guardian = nil)
user_guardian ||= guardian
DiscourseTagging.filter_visible(custom_sidebar_tags, user_guardian)
@@ -1944,7 +1951,7 @@ class User < ActiveRecord::Base
end
end
- SidebarSectionLink.insert_all!(records) if records.present?
+ SidebarSectionLink.insert_all(records) if records.present?
end
def stat
diff --git a/app/models/user_option.rb b/app/models/user_option.rb
index 8add4161d9..1bf0062d20 100644
--- a/app/models/user_option.rb
+++ b/app/models/user_option.rb
@@ -88,6 +88,8 @@ class UserOption < ActiveRecord::Base
self.title_count_mode = SiteSetting.default_title_count_mode
+ self.hide_profile_and_presence = SiteSetting.default_hide_profile_and_presence
+
true
end
@@ -232,45 +234,52 @@ end
#
# Table name: user_options
#
-# user_id :integer not null, primary key
-# mailing_list_mode :boolean default(FALSE), not null
-# email_digests :boolean
-# external_links_in_new_tab :boolean default(FALSE), not null
-# enable_quoting :boolean default(TRUE), not null
-# dynamic_favicon :boolean default(FALSE), not null
-# automatically_unpin_topics :boolean default(TRUE), not null
-# digest_after_minutes :integer
-# auto_track_topics_after_msecs :integer
-# new_topic_duration_minutes :integer
-# last_redirected_to_top_at :datetime
-# email_previous_replies :integer default(2), not null
-# email_in_reply_to :boolean default(TRUE), not null
-# like_notification_frequency :integer default(1), not null
-# mailing_list_mode_frequency :integer default(1), not null
-# include_tl0_in_digests :boolean default(FALSE)
-# notification_level_when_replying :integer
-# theme_key_seq :integer default(0), not null
-# allow_private_messages :boolean default(TRUE), not null
-# homepage_id :integer
-# theme_ids :integer default([]), not null, is an Array
-# hide_profile_and_presence :boolean default(FALSE), not null
-# text_size_key :integer default(0), not null
-# text_size_seq :integer default(0), not null
-# email_level :integer default(1), not null
-# email_messages_level :integer default(0), not null
-# title_count_mode_key :integer default(0), not null
-# enable_defer :boolean default(FALSE), not null
-# timezone :string
-# enable_allowed_pm_users :boolean default(FALSE), not null
-# dark_scheme_id :integer
-# skip_new_user_tips :boolean default(FALSE), not null
-# color_scheme_id :integer
-# default_calendar :integer default("none_selected"), not null
-# oldest_search_log_date :datetime
-# bookmark_auto_delete_preference :integer default(3), not null
-# enable_experimental_sidebar :boolean default(FALSE)
-# seen_popups :integer is an Array
-# sidebar_list_destination :integer default("none_selected"), not null
+# user_id :integer not null, primary key
+# mailing_list_mode :boolean default(FALSE), not null
+# email_digests :boolean
+# external_links_in_new_tab :boolean default(FALSE), not null
+# enable_quoting :boolean default(TRUE), not null
+# dynamic_favicon :boolean default(FALSE), not null
+# automatically_unpin_topics :boolean default(TRUE), not null
+# digest_after_minutes :integer
+# auto_track_topics_after_msecs :integer
+# new_topic_duration_minutes :integer
+# last_redirected_to_top_at :datetime
+# email_previous_replies :integer default(2), not null
+# email_in_reply_to :boolean default(TRUE), not null
+# like_notification_frequency :integer default(1), not null
+# mailing_list_mode_frequency :integer default(1), not null
+# include_tl0_in_digests :boolean default(FALSE)
+# notification_level_when_replying :integer
+# theme_key_seq :integer default(0), not null
+# allow_private_messages :boolean default(TRUE), not null
+# homepage_id :integer
+# theme_ids :integer default([]), not null, is an Array
+# hide_profile_and_presence :boolean default(FALSE), not null
+# text_size_key :integer default(0), not null
+# text_size_seq :integer default(0), not null
+# email_level :integer default(1), not null
+# email_messages_level :integer default(0), not null
+# title_count_mode_key :integer default(0), not null
+# enable_defer :boolean default(FALSE), not null
+# timezone :string
+# enable_allowed_pm_users :boolean default(FALSE), not null
+# dark_scheme_id :integer
+# skip_new_user_tips :boolean default(FALSE), not null
+# color_scheme_id :integer
+# default_calendar :integer default("none_selected"), not null
+# chat_enabled :boolean default(TRUE), not null
+# only_chat_push_notifications :boolean
+# oldest_search_log_date :datetime
+# chat_sound :string
+# dismissed_channel_retention_reminder :boolean
+# dismissed_dm_retention_reminder :boolean
+# bookmark_auto_delete_preference :integer default(3), not null
+# ignore_channel_wide_mention :boolean
+# chat_email_frequency :integer default(1), not null
+# enable_experimental_sidebar :boolean default(FALSE)
+# seen_popups :integer is an Array
+# sidebar_list_destination :integer default("none_selected"), not null
#
# Indexes
#
diff --git a/app/models/username_validator.rb b/app/models/username_validator.rb
index 1bb51491e6..747eb2f777 100644
--- a/app/models/username_validator.rb
+++ b/app/models/username_validator.rb
@@ -138,6 +138,6 @@ class UsernameValidator
end
def self.allowed_char?(c)
- c.match?(/[\w.-]/) || c.match?(SiteSetting.allowed_unicode_username_characters)
+ c.match?(/[\w.-]/) || c.match?(SiteSetting.allowed_unicode_username_characters_regex)
end
end
diff --git a/app/serializers/admin_detailed_user_serializer.rb b/app/serializers/admin_detailed_user_serializer.rb
index aab939247c..454fcf6cbd 100644
--- a/app/serializers/admin_detailed_user_serializer.rb
+++ b/app/serializers/admin_detailed_user_serializer.rb
@@ -45,7 +45,7 @@ class AdminDetailedUserSerializer < AdminUserSerializer
has_many :groups, embed: :object, serializer: BasicGroupSerializer
def second_factor_enabled
- object.totp_enabled? || object.security_keys_enabled?
+ object.totp_enabled? || object.security_keys_enabled? || object.backup_codes_enabled?
end
def can_disable_second_factor
diff --git a/app/serializers/current_user_serializer.rb b/app/serializers/current_user_serializer.rb
index 0a607648e7..70920d01a5 100644
--- a/app/serializers/current_user_serializer.rb
+++ b/app/serializers/current_user_serializer.rb
@@ -170,7 +170,7 @@ class CurrentUserSerializer < BasicUserSerializer
end
def can_send_private_messages
- scope.can_send_private_message?(Discourse.system_user)
+ scope.can_send_private_messages?
end
def can_edit
@@ -293,7 +293,7 @@ class CurrentUserSerializer < BasicUserSerializer
end
def include_seen_popups?
- SiteSetting.enable_onboarding_popups
+ SiteSetting.enable_user_tips
end
def include_primary_group_id?
@@ -323,7 +323,7 @@ class CurrentUserSerializer < BasicUserSerializer
end
def second_factor_enabled
- object.totp_enabled? || object.security_keys_enabled?
+ object.totp_enabled? || object.security_keys_enabled? || object.backup_codes_enabled?
end
def featured_topic
diff --git a/app/serializers/site_serializer.rb b/app/serializers/site_serializer.rb
index de82619c9f..30636fe61f 100644
--- a/app/serializers/site_serializer.rb
+++ b/app/serializers/site_serializer.rb
@@ -6,7 +6,7 @@ class SiteSerializer < ApplicationSerializer
:default_archetype,
:notification_types,
:post_types,
- :onboarding_popup_types,
+ :user_tips,
:trust_levels,
:groups,
:filters,
@@ -104,12 +104,12 @@ class SiteSerializer < ApplicationSerializer
Post.types
end
- def onboarding_popup_types
- OnboardingPopup.types
+ def user_tips
+ User.user_tips
end
- def include_onboarding_popup_types?
- SiteSetting.enable_onboarding_popups
+ def include_user_tips?
+ SiteSetting.enable_user_tips
end
def filters
diff --git a/app/serializers/user_card_serializer.rb b/app/serializers/user_card_serializer.rb
index a109e9ebbf..5e208169c7 100644
--- a/app/serializers/user_card_serializer.rb
+++ b/app/serializers/user_card_serializer.rb
@@ -141,7 +141,7 @@ class UserCardSerializer < BasicUserSerializer
# Needed because 'send_private_message_to_user' will always return false
# when the current user is being serialized
def can_send_private_messages
- scope.can_send_private_message?(Discourse.system_user)
+ scope.can_send_private_messages?
end
def can_send_private_message_to_user
diff --git a/app/serializers/user_serializer.rb b/app/serializers/user_serializer.rb
index 0e9f975032..eb2db4e56e 100644
--- a/app/serializers/user_serializer.rb
+++ b/app/serializers/user_serializer.rb
@@ -105,7 +105,7 @@ class UserSerializer < UserCardSerializer
end
def second_factor_enabled
- object.totp_enabled? || object.security_keys_enabled?
+ object.totp_enabled? || object.security_keys_enabled? || object.backup_codes_enabled?
end
def include_second_factor_backup_enabled?
diff --git a/app/services/user_updater.rb b/app/services/user_updater.rb
index ad7271c067..0083ff6955 100644
--- a/app/services/user_updater.rb
+++ b/app/services/user_updater.rb
@@ -181,11 +181,7 @@ class UserUpdater
end
if attributes.key?(:skip_new_user_tips)
- user.user_option.seen_popups = if user.user_option.skip_new_user_tips
- OnboardingPopup.types.values
- else
- nil
- end
+ user.user_option.seen_popups = user.user_option.skip_new_user_tips ? [-1] : nil
end
# automatically disable digests when mailing_list_mode is enabled
diff --git a/app/views/common/_discourse_publish_stylesheet.html.erb b/app/views/common/_discourse_publish_stylesheet.html.erb
index ae48d98f98..9429b0cd8d 100644
--- a/app/views/common/_discourse_publish_stylesheet.html.erb
+++ b/app/views/common/_discourse_publish_stylesheet.html.erb
@@ -1,11 +1,5 @@
<%= discourse_stylesheet_link_tag 'publish', theme_id: nil %>
-<%- if rtl? %>
- <%= discourse_stylesheet_link_tag(mobile_view? ? :publish_mobile_rtl : :publish_mobile_rtl) %>
-<%- else %>
- <%= discourse_stylesheet_link_tag(mobile_view? ? :publish_mobile : :publish_desktop) %>
-<%- end %>
-
<%- Discourse.find_plugin_css_assets(include_official: allow_plugins?, include_unofficial: allow_third_party_plugins?, mobile_view: mobile_view?, desktop_view: !mobile_view?, request: request).each do |file| %>
<%= discourse_stylesheet_link_tag(file) %>
<%- end %>
diff --git a/app/views/qunit/theme.html.erb b/app/views/qunit/theme.html.erb
index f2db3572fb..84397a564a 100644
--- a/app/views/qunit/theme.html.erb
+++ b/app/views/qunit/theme.html.erb
@@ -13,6 +13,7 @@
<%- end %>
<%= preload_script "test-support" %>
<%= preload_script "test-helpers" %>
+ <%= preload_script "test-site-settings" %>
<%= theme_translations_lookup %>
<%= theme_js_lookup %>
<%= theme_lookup("head_tag") %>
diff --git a/app/views/users/password_reset.html.erb b/app/views/users/password_reset.html.erb
index 9b9a5268f6..2cb06b1add 100644
--- a/app/views/users/password_reset.html.erb
+++ b/app/views/users/password_reset.html.erb
@@ -1,16 +1,20 @@
%{username} %{description}"
invited_to_topic: "%{username} %{description}"
invitee_accepted: "قَبِل %{username} دعوتك"
+ invitee_accepted_your_invitation: "قبل دعوتك"
moved_post: "نَقَل %{username} المنشور %{description}"
linked: "%{username} %{description}"
granted_badge: "تم منحك شارة \"%{description}\""
@@ -2447,12 +2575,26 @@ ar:
dismiss_confirmation:
body:
default:
- zero: "هل أنت متأكد؟ لديك %{count} من الإشعارات الهامة."
- one: "هل أنت متأكد؟ لديك %{count} إشعار مهم."
- two: "هل أنت متأكد؟ لديك %{count} إشعاران هامان."
- few: "هل أنت متأكد؟ لديك %{count} من الإشعارات الهامة."
- many: "هل أنت متأكد؟ لديك %{count} إشعارات مهمة."
- other: "هل أنت متأكد؟ لديك %{count} من الإشعارات الهامة."
+ zero: "هل أنت متأكد؟ لديك %{count} من الإشعارات المهمة."
+ one: "هل أنت متأكد؟ لديك إشعار واحد (%{count}) مهم."
+ two: "هل أنت متأكد؟ لديك إشعاران (%{count}) مهمان."
+ few: "هل أنت متأكد؟ لديك %{count} إشعارات مهمة."
+ many: "هل أنت متأكد؟ لديك %{count} إشعارًا مهمًا."
+ other: "هل أنت متأكد؟ لديك %{count} إشعار مهم."
+ bookmarks:
+ zero: "هل أنت متأكد؟ لديك %{count} تذكير بإشارة مرجعية غير مقروء."
+ one: "هل أنت متأكد؟ لديك تذكير واحد (%{count}) بإشارة مرجعية غير مقروء."
+ two: "هل أنت متأكد؟ لديك تذكيران (%{count}) بإشارة مرجعية غير مقروءين."
+ few: "هل أنت متأكد؟ لديك %{count} تذكيرات بإشارة مرجعية غير مقروءة."
+ many: "هل أنت متأكد؟ لديك %{count} تذكيرًا بإشارة مرجعية غير مقروء."
+ other: "هل أنت متأكد؟ لديك %{count} تذكير بإشارة مرجعية غير مقروء."
+ messages:
+ zero: "هل أنت متأكد؟ لديك %{count} رسالة شخصية غير مقروءة."
+ one: "هل أنت متأكد؟ لديك رسالة شخصية واحدة (%{count}) غير مقروءة."
+ two: "هل أنت متأكد؟ لديك رسالتان (%{count}) شخصيتان غير مقروءتين."
+ few: "هل أنت متأكد؟ لديك %{count} رسائل شخصية غير مقروءة."
+ many: "هل أنت متأكد؟ لديك %{count} رسالة شخصية غير مقروءة."
+ other: "هل أنت متأكد؟ لديك %{count} رسالة شخصية غير مقروءة."
dismiss: "تجاهل"
cancel: "إلغاء"
group_message_summary:
@@ -2514,9 +2656,9 @@ ar:
select_all: "تحديد الكل"
clear_all: "مسح الكل"
too_short: "عبارة البحث قصيرة جدًا."
- open_advanced: "فتح البحث المتقدم"
+ open_advanced: "فتح بحث متقدم"
clear_search: "مسح البحث"
- sort_or_bulk_actions: "فرز النتائج أو تحديدها بشكل جماعي"
+ sort_or_bulk_actions: "ترتيب النتائج أو تحديدها بشكلٍ جماعي"
result_count:
zero: "%{count}%{plus} نتيجة بحث عن العبارة %{term}"
one: "نتيجة بحث واحدة (%{count}) عن العبارة %{term}"
@@ -2542,19 +2684,21 @@ ar:
tags: "الوسوم"
in: "في"
in_this_topic: "في هذا الموضوع"
- in_this_topic_tooltip: "التبديل إلى البحث في جميع الموضوعات"
- in_topics_posts: "في جميع الموضوعات والمشاركات"
+ in_this_topic_tooltip: "التبديل إلى البحث في كل الموضوعات"
+ in_messages: "في الرسائل"
+ in_messages_tooltip: "التبديل إلى البحث في الموضوعات العادية"
+ in_topics_posts: "في كل الموضوعات والمنشورات"
enter_hint: "أو اضغط على Enter"
- in_posts_by: "في المشاركات بواسطة %{username}"
+ in_posts_by: "في منشورات %{username}"
browser_tip: "%{modifier} + f"
browser_tip_description: "مرة أخرى لاستخدام بحث المتصفح الأصلي"
recent: "عمليات البحث الأخيرة"
clear_recent: "مسح عمليات البحث الأخيرة"
type:
- default: "المواضيع/المشاركات"
+ default: "الموضوعات/المنشورات"
users: "المستخدمون"
categories: "الفئات"
- categories_and_tags: "التصنيفات/الوسوم"
+ categories_and_tags: "الفئات/الوسوم"
context:
user: "البحث عن المنشورات باسم المستخدم @%{username}"
category: "البحث في الفئة #%{category}"
@@ -2562,17 +2706,17 @@ ar:
topic: "البحث في هذا الموضوع"
private_messages: "البحث في الرسائل"
tips:
- category_tag: "فرز حسب التصنيف أو الوسم"
- author: "فرز حسب كاتب المشاركة"
- in: "فرز بواسطة بيانات التعريف (مثلاً: in:title, in:personal, in:pinne)"
- status: "فرز حسب حالة الموضوع"
- full_search: "تشغيل البحث الكامل في الصفحة"
+ category_tag: "يقوم بالتصفية حسب الفئة أو الوسم"
+ author: "يقوم بالتصفية حسب مؤلف المنشور"
+ in: "يقوم بالتصفية حسب البيانات الوصفية (مثلًا: in:title، in:personal، in:pinned)"
+ status: "يقوم بالتصفية حسب حالة الموضوع"
+ full_search: "يشغِّل البحث في الصفحة بأكملها"
full_search_key: "%{modifier} + Enter"
advanced:
- title: فلاتر متقدمة
+ title: عوامل تصفية متقدمة
posted_by:
label: تم النشر بواسطة
- aria_label: فرز حسب كاتب المشاركة
+ aria_label: التصفية حسب كاتب المنشور
in_category:
label: في الفئة
in_group:
@@ -2581,7 +2725,7 @@ ar:
label: باستخدام الشارة
with_tags:
label: بالوسوم
- aria_label: فرز باستخدام الوسوم
+ aria_label: التصفية باستخدام الوسوم
filters:
label: تقييد نتائج البحث على الموضوعات/المنشورات التي...
title: مطابقة العنوان فقط
@@ -2612,25 +2756,25 @@ ar:
label: المنشورات
min:
placeholder: الحد الأدنى
- aria_label: فرز حسب الحد الأدنى لعدد المشاركات
+ aria_label: التصفية حسب أدنى عدد من المنشورات
max:
placeholder: الحد الأقصى
- aria_label: فرز حسب الحد الأقصى لعدد المشاركات
+ aria_label: التصفية حسب أقصى عدد من المنشورات
time:
label: تاريخ النشر
- aria_label: فرز حسب تاريخ المنشور
+ aria_label: التصفية حسب تاريخ النشر
before: قبل
after: بعد
views:
label: مرات العرض
min_views:
placeholder: الحد الأدنى
- aria_label: فرز حسب الحد الأدنى من المشاهدات
+ aria_label: التصفية حسب أدنى عدد من المشاهدات
max_views:
placeholder: الحد الأقصى
- aria_label: فرز حسب الحد الأقصى من المشاهدات
+ aria_label: التصفية حسب أقصى عدد من المشاهدات
additional_options:
- label: "فرز حسب عدد المشاركات وعدد مشاهدات الموضوع"
+ label: "التصفية حسب عدد المنشورات وعدد مشاهدات الموضوع"
hamburger_menu: "القائمة"
new_item: "جديد"
go_back: "الرجوع"
@@ -2638,12 +2782,87 @@ ar:
current_user: "الانتقال إلى صفحة المستخدم"
view_all: "عرض الكل %{tab}"
user_menu:
+ generic_no_items: "لا توجد عناصر في هذه القائمة."
+ sr_menu_tabs: "علامات تبويب قائمة المستخدم"
+ view_all_notifications: "عرض كل الإشعارات"
+ view_all_bookmarks: "عرض كل الإشارات المرجعية"
+ view_all_messages: "عرض كل الرسائل الشخصية"
tabs:
+ all_notifications: "كل الإشعارات"
replies: "الردود"
+ replies_with_unread:
+ zero: "الردود - %{count} رد غير مقروء"
+ one: "الردود - رد واحد (%{count}) غير مقروء"
+ two: "الردود - ردَّان (%{count}) غير مقروءين"
+ few: "الردود - %{count} ردود غير مقروءة"
+ many: "الردود - %{count} ردًا غير مقروء"
+ other: "الردود - %{count} رد غير مقروء"
mentions: "الإشارات"
- likes: "الاعجابات"
+ mentions_with_unread:
+ zero: "الإشارات - %{count} إشارة غير مقروءة"
+ one: "الإشارات - إشارة واحدة (%{count}) غير مقروءة"
+ two: "الإشارات - إشارتان (%{count}) غير مقروءتين"
+ few: "الإشارات - %{count} إشارات غير مقروءة"
+ many: "الإشارات - %{count} إشارة غير مقروءة"
+ other: "الإشارات - %{count} إشارة غير مقروءة"
+ likes: "تسجيلات الإعجاب"
+ likes_with_unread:
+ zero: "تسجيلات الإعجاب - %{count} إعجاب غير مقروء"
+ one: "تسجيلات الإعجاب - إعجاب واحد (%{count}) غير مقروء"
+ two: "تسجيلات الإعجاب - إعجابان (%{count}) غير مقروءين"
+ few: "تسجيلات الإعجاب - %{count} إعجابات غير مقروءة"
+ many: "تسجيلات الإعجاب - %{count} إعجابًا غير مقروء"
+ other: "تسجيلات الإعجاب - %{count} إعجاب غير مقروء"
+ watching: "الموضوعات المراقبة"
+ watching_with_unread:
+ zero: "الموضوعات المراقبة - %{count} موضوع غير مراقب"
+ one: "الموضوعات المراقبة - موضوع واحد (%{count}) غير مراقب"
+ two: "الموضوعات المراقبة - موضوعان (%{count}) غير مراقبين"
+ few: "الموضوعات المراقبة - %{count} موضوعات غير مراقبة"
+ many: "الموضوعات المراقبة - %{count} موضوعًا غير مراقب"
+ other: "الموضوعات المراقبة - %{count} موضوع غير مراقب"
+ messages: "الرسائل الشخصية"
+ messages_with_unread:
+ zero: "الرسائل الشخصية - %{count} رسالة غير مقروءة"
+ one: "الرسائل الشخصية - رسالة واحدة (%{count}) غير مقروءة"
+ two: "الرسائل الشخصية - رسالتان (%{count}) غير مقروءتين"
+ few: "الرسائل الشخصية - %{count} رسائل غير مقروءة"
+ many: "الرسائل الشخصية - %{count} رسالة غير مقروءة"
+ other: "الرسائل الشخصية - %{count} رسالة غير مقروءة"
bookmarks: "الإشارات المرجعية"
+ bookmarks_with_unread:
+ zero: "الإشارات المرجعية - %{count} إشارة مرجعية غير مقروءة"
+ one: "الإشارات المرجعية - إشارة مرجعية واحدة (%{count}) غير مقروءة"
+ two: "الإشارات المرجعية - إشارتان مرجعيتان (%{count}) غير مقروءتين"
+ few: "الإشارات المرجعية - %{count} إشارات مرجعية غير مقروءة"
+ many: "الإشارات المرجعية - %{count} إشارة مرجعية غير مقروءة"
+ other: "الإشارات المرجعية - %{count} إشارة مرجعية غير مقروءة"
+ review_queue: "قائمة انتظار المراجعة"
+ review_queue_with_unread:
+ zero: "قائمة انتظار المراجعة - %{count} عنصر بحاجة إلى المراجعة"
+ one: "قائمة انتظار المراجعة - عنصر واحد (%{count}) بحاجة إلى المراجعة"
+ two: "قائمة انتظار المراجعة - عنصران (%{count}) بحاجة إلى المراجعة"
+ few: "قائمة انتظار المراجعة - %{count} عناصر بحاجة إلى المراجعة"
+ many: "قائمة انتظار المراجعة - %{count} عنصرًا بحاجة إلى المراجعة"
+ other: "قائمة انتظار المراجعة - %{count} عنصر بحاجة إلى المراجعة"
+ other_notifications: "إشعارات أخرى"
+ other_notifications_with_unread:
+ zero: "إشعارات أخرى - %{count} إشعار غير مقروء"
+ one: "إشعارات أخرى - إشعار واحد (%{count}) غير مقروء"
+ two: "إشعارات أخرى - إشعاران (%{count}) غير مقروءين"
+ few: "إشعارات أخرى - %{count} إشعارات غير مقروءة"
+ many: "إشعارات أخرى - %{count} إشعارًا غير مقروء"
+ other: "إشعارات أخرى - %{count} إشعار غير مقروء"
profile: "الملف الشخصي"
+ reviewable:
+ view_all: "عرض كل عناصر المراجعة"
+ queue: "قائمة الانتظار"
+ deleted_user: "(مستخدم محذوف)"
+ deleted_post: "(منشور محذوف)"
+ post_number_with_topic_title: "المنشور #%{post_number} - %{title}"
+ new_post_in_topic: "منشور جديد في %{title}"
+ user_requires_approval: "%{username} يتطلب الموافقة"
+ default_item: "العنصر القابل للمراجعة #%{reviewable_id}"
topics:
new_messages_marker: "آخر زيارة"
bulk:
@@ -2651,10 +2870,11 @@ ar:
clear_all: "مسح الكل"
unlist_topics: "إلغاء إدراج الموضوعات"
relist_topics: "إعادة إدراج الموضوعات"
+ reset_bump_dates: "إعادة تعيين تواريخ الرفع"
defer: "تأجيل"
delete: "حذف الموضوعات"
dismiss: "تجاهل"
- dismiss_read: "تجاهل جميع الموضوعات غير المقروءة"
+ dismiss_read: "تجاهل كل الموضوعات غير المقروءة"
dismiss_read_with_selected:
zero: "تجاهل %{count} غير مقروءة"
one: "تجاهل %{count} غير مقروءة"
@@ -2682,7 +2902,7 @@ ar:
other: "تجاهل الجديدة (%{count})"
toggle: "تفعيل التحديد الجماعي للموضوعات"
actions: "الإجراءات الجماعية"
- change_category: "تعيين التصنيف..."
+ change_category: "ضبط الفئة..."
close_topics: "إغلاق الموضوعات"
archive_topics: "أرشفة الموضوعات"
move_messages_to_inbox: "النقل إلى صندوق الوارد"
@@ -2718,7 +2938,7 @@ ar:
other: "التقدُّم: %{count} موضوع"
none:
unread: "ليس لديك أي موضوع غير مقروء."
- unseen: "ليس لديك مواضيع غير مقروءة."
+ unseen: "ليس لديك موضوعات غير مقروءة."
new: "ليس لديك أي موضوع جديد."
read: "لم تقرأ أي موضوع بعد."
posted: "لم تنشر في أي موضوع بعد."
@@ -2983,6 +3203,8 @@ ar:
unpin: "إلغاء تثبيت الموضوع"
unarchive: "إلغاء أرشفة الموضوع"
archive: "أرشفة الموضوع"
+ invisible: "إلغاء إدراج الموضوع"
+ visible: "إدارج الموضوع"
reset_read: "إعادة ضبط بيانات القراءة"
make_public: "التحويل إلى موضوع عام..."
make_private: "التحويل إلى رسالة خاصة"
@@ -2997,16 +3219,17 @@ ar:
title: "الرد"
help: "ابدأ في كتابة رد على هذه الموضوع"
share:
+ title: "مشاركة الموضوع"
extended_title: "مشاركة رابط"
help: "شارِك رابطًا إلى هذا الموضوع"
instructions: "مشاركة رابط إلى هذا الموضوع:"
copied: "تم نسخ رابط الدعوة."
restricted_groups:
zero: "مرئي فقط لأعضاء المجموعة: %{groupNames}"
- one: "مرئي فقط لعضو المجموعة: %{groupNames}"
- two: "مرئي فقط لعضوان المجموعة: %{groupNames}"
- few: "مرئي فقط لأعضاء المجموعة: %{groupNames}"
- many: "مرئي فقط لأعضاء المجموعة: %{groupNames}"
+ one: "مرئي فقط لأعضاء المجموعة: %{groupNames}"
+ two: "مرئي فقط لأعضاء المجموعتَين: %{groupNames}"
+ few: "مرئي فقط لأعضاء المجموعات: %{groupNames}"
+ many: "مرئي فقط لأعضاء المجموعات: %{groupNames}"
other: "مرئي فقط لأعضاء المجموعات: %{groupNames}"
invite_users: "دعوة"
print:
@@ -3222,6 +3445,7 @@ ar:
other: "لقد حدَّدت %{count} منشور."
deleted_by_author_simple: "(تم حذف الموضوع بواسطة الكاتب)"
post:
+ confirm_delete: "هل تريد بالتأكيد حذف هذا المنشور؟"
quote_reply: "اقتباس"
quote_reply_shortcut: "أو اضغط على q"
quote_edit: "تعديل"
@@ -3240,7 +3464,18 @@ ar:
show_hidden: "عرض المحتوى الذي تم تجاهله"
deleted_by_author_simple: "(تم حذف المنشور بواسطة الكاتب)"
collapse: "طي"
+ sr_collapse_replies: "طي الردود المضمَّنة"
+ sr_date: "تاريخ المنشور"
+ sr_expand_replies:
+ zero: "هناك %{count} رد على هذا المنشور. انقر للتوسيع"
+ one: "هناك رد واحد (%{count}) على هذا المنشور. انقر للتوسيع"
+ two: "هناك ردَّان (%{count}) على هذا المنشور. انقر للتوسيع"
+ few: "هناك %{count} ردود على هذا المنشور. انقر للتوسيع"
+ many: "هناك %{count} ردًا على هذا المنشور. انقر للتوسيع"
+ other: "هناك %{count} رد على هذا المنشور. انقر للتوسيع"
expand_collapse: "توسيع/طي"
+ sr_below_embedded_posts_description: "ردود المنشور #%{post_number}"
+ sr_embedded_reply_description: "رد بواسطة @%{username} على المنشور #%{post_number}"
locked: "قفل أحد أعضاء فريق العمل تعديل هذه المشاركة"
gap:
zero: "عرض %{count} رد مخفي"
@@ -3249,6 +3484,7 @@ ar:
few: "عرض %{count} ردود مخفية"
many: "عرض %{count} ردًا مخفيًا"
other: "عرض %{count} رد مخفي"
+ sr_reply_to: "رد على المنشور #%{post_number} بواسطة @%{username}"
notice:
new_user: "هذه أول مرة ينشر فيها %{user} شيئًا؛ فلنرحِّب به في مجتمعنا!"
returning_user: "مرَّ وقت طويل منذ أن قرأنا شيئًا من %{user}؛ إذ كان آخر منشور له في %{time}."
@@ -3277,6 +3513,20 @@ ar:
few: "لقد سجَّلت إعجابك أنت و%{count} أشخاص آخرين بهذا المنشور"
many: "لقد سجَّلت إعجابك أنت و%{count} شخصًا آخر بهذا المنشور"
other: "لقد سجَّلت إعجابك أنت و%{count} شخص آخر بهذا المنشور"
+ sr_post_like_count_button:
+ zero: "%{count} شخص أعجبه هذا المنشور. انقر للعرض"
+ one: "شخص واحد (%{count}) أعجبه هذا المنشور. انقر للعرض"
+ two: "شخصان (%{count}) أعجبهما هذا المنشور. انقر للعرض"
+ few: "%{count} أشخاص أعجبهم هذا المنشور. انقر للعرض"
+ many: "%{count} شخصًا أعجبه هذا المنشور. انقر للعرض"
+ other: "%{count} شخص أعجبه هذا المنشور. انقر للعرض"
+ sr_post_read_count_button:
+ zero: "%{count} شخص قرأ هذا المنشور. انقر للعرض"
+ one: "شخص واحد (%{count}) قرأ هذا المنشور. انقر للعرض"
+ two: "شخصان (%{count}) قرآ هذا المنشور. انقر للعرض"
+ few: "%{count} أشخاص قرؤوا هذا المنشور. انقر للعرض"
+ many: "%{count} شخصًا قرأ هذا المنشور. انقر للعرض"
+ other: "%{count} شخص قرأ هذا المنشور. انقر للعرض"
filtered_replies_hint:
zero: "عرض هذا المنشور وردوده %{count}"
one: "عرض هذا المنشور والرد عليه"
@@ -3298,7 +3548,8 @@ ar:
edit: "عذرا، حدث خطأ في أثناء تعديل منشورك. يُرجى إعادة المحاولة."
upload: "عذرًا، حدث خطأ في أثناء تحميل هذا الملف. يُرجى إعادة المحاولة."
file_too_large: "عذرًا، لكن الملف كبير جدًا (أقصى حجم هو %{max_size_kb} ك.ب). لماذا لا تحمِّل ملفك الكبير إلى خدمة مشاركة سحابية، ثم تلصق الرابط؟"
- file_too_large_humanized: "عذرًا، هذا الملف كبير جدًا (الحد الأقصى للحجم هو %{max_size}). لماذا لا تقوم بتحميل ملفك الكبير إلى خدمة مشاركة سحابية، ثم قم بلصق الرابط؟"
+ file_size_zero: "عذرًا، يبدو أنه حدث خطأ ما؛ فحجم الملف الذي تحاول تحميله هو 0 بايت. حاول مرة أخرى."
+ file_too_large_humanized: "عذرًا، هذا الملف كبير جدًا (الحد الأقصى للحجم هو %{max_size}). لماذا لا تقوم بتحميل ملفك الكبير إلى خدمة مشاركة سحابية، ثم تلصق الرابط؟"
too_many_uploads: "عذرًا، يمكنك تحميل ملف واحد فقط في الوقت نفسه."
too_many_dragged_and_dropped_files:
zero: "عذرًا، يمكنك تحميل %{count} ملف فقط في الوقت نفسه."
@@ -3355,7 +3606,7 @@ ar:
just_the_post: "لا، هذا المنشور فقط"
admin: "إجراءات المسؤول على المنشور"
permanently_delete: "الحذف بشكلٍ دائم"
- permanently_delete_confirmation: "هل أنت متأكد من أنك تريد حذف هذا المنشور بشكل دائم؟ لن تتمكن من استعادته."
+ permanently_delete_confirmation: "هل تريد بالتأكيد حذف هذا المنشور بشكلٍ دائم؟ لن تتمكن من استعادته."
wiki: "التحويل إلى Wiki"
unwiki: "إزالة Wiki"
convert_to_moderator: "لون إضافة فريق العمل"
@@ -3417,6 +3668,8 @@ ar:
few: "و%{count} آخرين قرؤوا ذلك"
many: "و%{count} آخرين قرؤوا ذلك"
other: "و%{count} آخرين قرؤوا ذلك"
+ sr_post_likers_list_description: "المستخدمون الذين أعجبهم هذا المنشور"
+ sr_post_readers_list_description: "المستخدمون الذين قرؤوا هذا المنشور"
by_you:
off_topic: "لقد أبلغت عن هذا المنشور على أنه خارج الموضوع"
spam: "لقد أبلغت عن هذا المنشور على أنه غير مرغوب فيه"
@@ -3431,6 +3684,14 @@ ar:
few: "هل تريد بالتأكيد حذف %{count} منشورات؟"
many: "هل تريد بالتأكيد حذف %{count} منشورًا؟"
other: "هل تريد بالتأكيد حذف %{count} منشور؟"
+ merge:
+ confirm:
+ zero: "هل تريد بالتأكيد دمج تلك المنشورات البالغ عددها %{count}؟"
+ one: "هل تريد بالتأكيد دمج تلك المنشورات البالغ عددها %{count}؟"
+ two: "هل تريد بالتأكيد دمج تلك المنشورات البالغ عددها %{count}؟"
+ few: "هل تريد بالتأكيد دمج تلك المنشورات البالغ عددها %{count}؟"
+ many: "هل تريد بالتأكيد دمج تلك المنشورات البالغ عددها %{count}؟"
+ other: "هل تريد بالتأكيد دمج تلك المنشورات البالغ عددها %{count}؟"
revisions:
controls:
first: "أول مراجعة"
@@ -3468,7 +3729,7 @@ ar:
create: "إنشاء إشارة مرجعية"
create_for_topic: "إنشاء إشارة مرجعية للموضوع"
edit: "تعديل الإشارة المرجعية"
- edit_for_topic: "تحرير الإشارة المرجعية للموضوع"
+ edit_for_topic: "تعديل الإشارة المرجعية للموضوع"
created: "تاريخ الإنشاء"
updated: "تاريخ التحديث"
name: "الاسم"
@@ -3483,6 +3744,7 @@ ar:
name: "تعديل الإشارة المرجعية"
description: "تعديل اسم الإشارة المرجعية أو تغيير تاريخ التذكير ووقته"
clear_bookmark_reminder:
+ name: "مسح التذكير"
description: "امسح تاريخ ووقت التذكير"
pin_bookmark:
name: "تثبيت الإشارة المرجعية"
@@ -3504,6 +3766,7 @@ ar:
all: "كل الفئات"
choose: "الفئة…"
edit: "تعديل"
+ edit_title: "تعديل هذه الفئة"
edit_dialog_title: "تعديل: %{categoryName}"
view: "عرض الموضوعات في الفئة"
back: "الرجوع إلى الفئة"
@@ -3519,10 +3782,10 @@ ar:
manage_tag_groups_link: "إدارة مجموعات الوسوم"
allow_global_tags_label: "السماح بالوسوم الأخرى أيضًا"
required_tag_group:
- description: "تتطلب موضوعات جديدة بها وسوم من مجموعات العلامات:"
+ description: "يلزم أن تحتوي الموضوعات الجديدة على وسوم من مجموعات الوسوم:"
delete: "حذف"
- add: "أضف مجموعة العلامات المطلوبة"
- placeholder: "حدد مجموعة العلامات ..."
+ add: "إضافة مجموعة الوسوم المطلوبة"
+ placeholder: "تحديد مجموعة الوسوم..."
topic_featured_link_allowed: "السماح بالروابط المميزة في هذه الفئة."
delete: "حذف الفئة"
create: "فئة جديدة"
@@ -3535,6 +3798,7 @@ ar:
name: "اسم الفئة"
description: "الوصف"
logo: "صورة شعار الفئة"
+ logo_dark: "صورة شعار فئة الوضع الداكن"
background_image: "صورة خلفية الفئة"
badge_colors: "ألوان الشارات"
background_color: "لون الخلفية"
@@ -3582,9 +3846,9 @@ ar:
default_list_filter: "تصفية القائمة الافتراضية:"
allow_badges_label: "السماح بمنح الشارات في هذه الفئة"
edit_permissions: "تعديل الأذونات"
- reviewable_by_group: "بالإضافة إلى فريق العمل، يمكن أيضًا مراجعة المحتوى في هذا التصنيف بواسطة:"
+ reviewable_by_group: "بالإضافة إلى فريق العمل، يمكن أيضًا مراجعة المحتوى في هذه الفئة من قِبل:"
review_group_name: "اسم المجموعة"
- require_topic_approval: "طلب موافقة المشرف على جميع الموضوعات الجديدة"
+ require_topic_approval: "طلب موافقة المشرف على كل الموضوعات الجديدة"
require_reply_approval: "طلب موافقة المشرف على جميع الردود الجديدة"
this_year: "هذا العام"
position: "الترتيب في صفحة الفئات:"
@@ -3597,16 +3861,16 @@ ar:
num_auto_bump_daily: "عدد الموضوعات المفتوحة التي سيتم رفعها تلقائيًا بشكل يومي:"
navigate_to_first_post_after_read: "الانتقال إلى أول منشور بعد قراءة الموضوعات"
notifications:
- title: "تغيير مستوى الإشعار لهذا التصنيف"
+ title: "تغيير مستوى الإشعار لهذه الفئة"
watching:
title: "المراقبة"
- description: "ستراقب تلقائيًا جميع الموضوعات في هذه الفئة. وسيتم إرسال إشعار إليك بكل منشور جديد في كل موضوع، وسيتم عرض عدد الردود الجديدة."
+ description: "ستراقب تلقائيًا كل الموضوعات في هذه الفئة. وسيتم إرسال إشعار إليك بكل منشور جديد في كل موضوع، وسيتم عرض عدد الردود الجديدة."
watching_first_post:
title: "مراقبة أول منشور"
description: "سنُرسل إليك إشعارًا بالموضوعات الجديدة في هذه الفئة، ولكن ليس الردود على الموضوعات."
tracking:
title: "التتبُّع"
- description: "ستتتبَّع تلقائيًا جميع الموضوعات في هذه الفئة. وسيتم إرسال إشعار إليك إذا أشار إليك شخص ما @name أو ردَّ عليك، وسيتم عرض عدد الردود الجديدة."
+ description: "ستتتبَّع تلقائيًا كل الموضوعات في هذه الفئة. وسيتم إرسال إشعار إليك إذا أشار إليك شخص ما @name أو ردَّ عليك، وسيتم عرض عدد الردود الجديدة."
regular:
title: "عادية"
description: "سنُرسل إليك إشعارًا إذا أشار أحد إلى اسمك باستخدام الرمز @ أو ردَّ عليك."
@@ -3624,8 +3888,8 @@ ar:
very_high: "مرتفعة جدًا"
sort_options:
default: "افتراضية"
- likes: "الإعجابات"
- op_likes: "الإعجابات على المنشور الأصلي"
+ likes: "تسجيلات الإعجاب"
+ op_likes: "تسجيلات الإعجاب على المنشور الأصلي"
views: "مرات العرض"
posts: "المنشورات"
activity: "النشاط"
@@ -3645,7 +3909,7 @@ ar:
appearance: "الظهور"
email: "البريد الإلكتروني"
list_filters:
- all: "جميع الموضوعات"
+ all: "كل الموضوعات"
none: "لا توجد فئات فرعية"
colors_disabled: "لا يمكنك تحديد الألوان لأنه ليس لديك نمط فئة."
flagging:
@@ -3764,6 +4028,7 @@ ar:
other {}}
original_post: "المنشور الأصلي"
views: "مرات العرض"
+ sr_views: "الترتيب حسب المشاهدات"
views_lowercase:
zero: "مرة عرض"
one: "مرة عرض واحدة"
@@ -3772,6 +4037,7 @@ ar:
many: "مرة عرض"
other: "مرة عرض"
replies: "الردود"
+ sr_replies: "الترتيب حسب الردود"
views_long:
zero: "لقد تم عرض هذا الموضوع %{number} مرة"
one: "لقد تم عرض هذا الموضوع مرة واحدة (%{count})"
@@ -3780,7 +4046,10 @@ ar:
many: "لقد تم عرض هذا الموضوع %{number} مرة"
other: "لقد تم عرض هذا الموضوع %{number} مرة"
activity: "النشاط"
- likes: "الإعجابات"
+ sr_activity: "الترتيب حسب النشاط"
+ likes: "تسجيلات الإعجاب"
+ sr_likes: "الترتيب حسب تسجيلات الإعجاب"
+ sr_op_likes: "الترتيب حسب تسجيلات الإعجاب على المنشور"
likes_lowercase:
zero: "مرة إعجاب"
one: "مرة إعجاب واحدة"
@@ -3798,7 +4067,7 @@ ar:
other: "مستخدم"
category_title: "الفئة"
history_capped_revisions: "السجل، آخر 100 مراجعة"
- history: "التاريخ"
+ history: "السجل"
changed_by: "بواسطة %{author}"
raw_email:
title: "البريد الوارد"
@@ -3823,7 +4092,7 @@ ar:
categories:
title: "الفئات"
title_in: "الفئة - %{categoryName}"
- help: "جميع الموضوعات مجمَّعة حسب الفئة"
+ help: "كل الموضوعات مجمَّعة حسب الفئة"
unread:
title: "غير المقروءة"
title_with_count:
@@ -3842,9 +4111,9 @@ ar:
many: "%{count} غير مقروءة"
other: "%{count} غير مقروءة"
unseen:
- title: "غير مرئي"
- lower_title: "غير مرئي"
- help: "الموضوعات والموضوعات الجديدة التي تشاهدها حاليًا أو تتبعها بمشاركات غير مقروءة"
+ title: "غير مقروءة"
+ lower_title: "غير مقروءة"
+ help: "الموضوعات الجديدة والموضوعات التي تراقبها حاليًا أو تتتبَّعها وبها منشورات غير مقروءة"
new:
lower_title_with_count:
zero: "%{count} جديد"
@@ -3901,6 +4170,8 @@ ar:
this_week: "الأسبوع"
today: "اليوم"
other_periods: "رؤية الأكثر نشاطًا:"
+ browser_update: 'عذرًا، متصفحك غير مدعوم. يُرجى التبديل إلى متصفح مدعوم لعرض المحتوى الغني، وتسجيل الدخول والرد.'
+ safari_13_warning: سيزيل هذا الموقع الدعم لإصدارات 13 من iOS وSafari، والإصدارات الأقدم. ستظل هناك نسخة مبسَّطة متاحة للقراءة فقط. (المزيد من المعلومات)
permission_types:
full: "الإنشاء/الرد/العرض"
create_post: "الرد/العرض"
@@ -3908,6 +4179,7 @@ ar:
preloader_text: "جارٍ التحميل"
lightbox:
download: "تنزيل"
+ open: "الصورة الأصلية"
previous: "السابق (مفتاح السهم الأيسر)"
next: "التالي (مفتاح السهم الأيمن)"
counter: "%curr% من %total%"
@@ -3922,6 +4194,7 @@ ar:
shortcut_delimiter_slash: "%{shortcut1}/%{shortcut2}"
shortcut_delimiter_space: "%{shortcut1} %{shortcut2}"
title: "اختصارات لوحة المفاتيح"
+ short_title: "الاختصارات"
jump_to:
title: "الانتقال إلى"
home: "%{shortcut} الصفحة الرئيسية"
@@ -3989,6 +4262,7 @@ ar:
edit: "%{shortcut} تعديل المنشور"
delete: "%{shortcut} حذف المنشور"
mark_muted: "%{shortcut} كتم الموضوع"
+ mark_regular: "%{shortcut} الموضوع العادي (الافتراضي)"
mark_tracking: "%{shortcut} تتبُّع الموضوع"
mark_watching: "%{shortcut} مراقبة الموضوع"
print: "%{shortcut} طباعة الموضوع"
@@ -3998,7 +4272,7 @@ ar:
title: "قائمة البحث"
prev_next: "%{shortcut} نقل التحديد لأعلى ولأسفل"
insert_url: "%{shortcut} إدخال التحديد في أداة الإنشاء المفتوحة"
- full_page_search: "%{shortcut} يطلق بحث كامل الصفحة"
+ full_page_search: "%{shortcut} يطلق بحثًا في كامل الصفحة"
badges:
earned_n_times:
zero: "تم منحك هذه الشارة %{count} مرة"
@@ -4055,14 +4329,15 @@ ar:
save_ics: "تنزيل ملف .ics"
save_google: "إضافة إلى تقويم Google"
remember: "لا تسألني مرة أخرى"
- remember_explanation: "(يمكنك تغيير هذا التفضيل في تفضيلات المستخدم الخاصة بك)"
+ remember_explanation: "(يمكنك تغيير هذا التفضيل في تفضيلات المستخدم لديك)"
download: "تنزيل"
default_calendar: "التقويم الافتراضي"
- default_calendar_instruction: "تحديد التقويم الذي يجب استخدامه عند حفظ التواريخ"
+ default_calendar_instruction: "حدِّد التقويم الذي ينبغي استخدامه عند حفظ التواريخ"
add_to_calendar: "إضافة إلى التقويم"
- google: "تقويم جوجل"
+ google: "تقويم Google"
ics: "ICS"
tagging:
+ all_tags: "كل الوسوم"
other_tags: "وسوم أخرى"
selector_all_tags: "كل الوسوم"
selector_no_tags: "لا توجد وسوم"
@@ -4070,19 +4345,26 @@ ar:
tags: "الوسوم"
choose_for_topic: "الوسوم الاختيارية"
choose_for_topic_required:
- zero: "حدد وسم %{count} على الأقل..."
- one: "يجب تحديد %{count} وسمًا على الأقل..."
- two: "يجب تحديد وسمَين %{count} على الأقل..."
- few: "يجب تحديد %{count} وسوم على الأقل..."
- many: "يجب تحديد %{count} وسمًا على الأقل..."
- other: "يجب تحديد %{count} وسمًا على الأقل..."
+ zero: "حدِّد %{count} وسم على الأقل..."
+ one: "حدِّد وسمًا واحدًا (%{count}) على الأقل..."
+ two: "حدِّد وسمَين (%{count}) على الأقل..."
+ few: "حدِّد %{count} وسوم على الأقل..."
+ many: "حدِّد %{count} وسمًا على الأقل..."
+ other: "حدِّد %{count} وسم على الأقل..."
+ choose_for_topic_required_group:
+ zero: "حدِّد %{count} وسم من '%{name}'..."
+ one: "حدِّد وسمًا واحدًا (%{count}) من '%{name}'..."
+ two: "حدِّد وسمين (%{count}) من '%{name}'..."
+ few: "حدِّد %{count} وسوم من '%{name}'..."
+ many: "حدِّد %{count} وسمًا من '%{name}'..."
+ other: "حدِّد %{count} وسم من '%{name}'..."
info: "المعلومات"
- default_info: "هذ الوسم ليس مقصورًا على أي تصنيف، وليس له مرادفات."
- staff_info: "لإضافة قيود، ضع هذه العلامة في مجموعة العلامة."
+ default_info: "هذ الوسم ليس مقصورًا على أي فئة، وليس له مرادفات."
+ staff_info: "لإضافة قيود، ضَع هذا الوسم في مجموعة الوسوم."
category_restricted: "هذا الوسم مقيَّد بالفئات التي ليس لديك إذن بالوصول إليها."
synonyms: "المرادفات"
synonyms_description: "عند استخدام الوسوم التالية، سيتم استبدالها بالوسم %{base_tag_name}."
- save: "حفظ الاسم ووصف الوسم"
+ save: "حفظ اسم الوسم ووصفه"
tag_groups_info:
zero: "هذا الوسم ينتمي إلى المجموعة: %{tag_groups}."
one: 'هذا الوسم ينتمي إلى المجموعة: %{tag_groups}.'
@@ -4097,7 +4379,7 @@ ar:
few: "لا يمكن استخدامه إلا في هذه الفئات:"
many: "لا يمكن استخدامه إلا في هذه الفئات:"
other: "لا يمكن استخدامه إلا في هذه الفئات:"
- edit_synonyms: "تحرير المرادفات"
+ edit_synonyms: "تعديل المرادفات"
add_synonyms_label: "إضافة المرادفات:"
add_synonyms: "إضافة"
add_synonyms_explanation:
@@ -4126,7 +4408,7 @@ ar:
few: "سيتم حذف مرادفاته (%{count}) أيضًا."
many: "سيتم حذف مرادفاته (%{count}) أيضًا."
other: "سيتم حذف مرادفاته (%{count}) أيضًا."
- edit_tag: "تحرير اسم الوسم والوصف"
+ edit_tag: "تعديل اسم الوسم ووصفه"
description: "الوصف"
sort_by: "الترتيب حسب:"
sort_by_count: "العدد"
@@ -4163,13 +4445,13 @@ ar:
notifications:
watching:
title: "المراقبة"
- description: "ستراقب تلقائيًا جميع الموضوعات التي تحمل هذا الوسم. وسنُرسل إليك إشعارات بالمنشورات والموضوعات الجديدة، وسيظهر أيضًا عدد المنشورات الجديدة بجانب الموضوع."
+ description: "ستراقب تلقائيًا كل الموضوعات التي تحمل هذا الوسم. وسنُرسل إليك إشعارات بالمنشورات والموضوعات الجديدة، وسيظهر أيضًا عدد المنشورات الجديدة بجانب الموضوع."
watching_first_post:
title: "مراقبة أول منشور"
description: "سنُرسل إليك إشعارًا بالموضوعات الجديدة التي تحمل هذا الوسم، ولكن ليس الردود على الموضوعات."
tracking:
title: "التتبُّع"
- description: "ستتتبَّع تلقائيًا جميع الموضوعات التي تحمل هذا الوسم. وسيظهر أيضًا عدد المنشورات غير المقروءة والجديدة بجانب الموضوع."
+ description: "ستتتبَّع تلقائيًا كل الموضوعات التي تحمل هذا الوسم. وسيظهر أيضًا عدد المنشورات غير المقروءة والجديدة بجانب الموضوع."
regular:
title: "عادية"
description: "سنُرسل إليك إشعارًا إذا أشار أحد إلى اسمك باستخدام الرمز @ أو ردَّ على منشورك."
@@ -4177,6 +4459,7 @@ ar:
title: "الكتم"
description: "لن تتلقى أي إشعارات أبدًا بخصوص الموضوعات الجديدة التي تحمل هذا الوسم، ولن تظهر في علامة تبويب الموضوعات غير المقروءة."
groups:
+ back_btn: "العودة إلى كل الوسوم"
title: "مجموعات الوسوم"
about_heading: "حدِّد مجموعة وسوم أو أنشئ مجموعة جديدة"
about_heading_empty: "أنشئ مجموعة وسوم جديدة للبدء"
@@ -4204,7 +4487,7 @@ ar:
topics:
none:
unread: "ليس لديك أي موضوع غير مقروء."
- unseen: "ليس لديك مواضيع غير مقروءة."
+ unseen: "ليس لديك موضوعات غير مقروءة."
new: "ليس لديك أي موضوع جديد."
read: "لم تقرأ أي موضوع بعد."
posted: "لم تنشر في أي موضوع بعد."
@@ -4249,23 +4532,29 @@ ar:
pick_files_button:
unsupported_file_picked: "لقد اخترت ملفًا غير مدعوم. أنواع الملفات المدعومة - %{types}."
user_activity:
- no_activity_title: "لا يوجد نشاط."
- no_replies_title: "لم ترد على أي مواضيع حتى الآن"
+ no_activity_title: "لا يوجد نشاط حتى الآن"
+ no_activity_body: "مرحبًا بك في مجتمعنا! أنت عضو جديد هنا ولم تساهم في المناقشات. كخطوة أولى، انتقل إلى الأعلى أو الفئات وابدأ القراءة! ضع %{heartIcon} على المنشورات التي تعجبك أو تريد معرفة المزيد عنها. وبينما تشارك، سيتم تسجيل نشاطك هنا."
+ no_replies_title: "لم ترد على أي موضوعات حتى الآن"
+ no_replies_title_others: "لم يرد %{username} على أي موضوعات بعد"
+ no_replies_body: "عندما تكتشف محادثة مثيرة للاهتمام ترغب في المساهمة فيها، اضغط على زر الرد تحت أي منشورة مباشرةً للبدء في الرد على هذا المنشور المعيَّن. أو، إذا كنت تفضل الرد على الموضوع العام بدلًا من الرد على منشور فردي أو شخص، فابحث عن زر الرد في أسفل الموضوع، أو تحت الجدول الزمني للموضوع."
no_drafts_title: "لم تبدأ أي مسودات"
- no_drafts_body: "لست مستعدًا للنشر؟ سنقوم تلقائيًا بحفظ مسودة جديدة وإدراجها هنا كلما بدأت في إنشاء موضوع أو رد أو رسالة شخصية. حدد زر الإلغاء لتجاهل المُسَوَّدَة أو حفظها للمتابعة لاحقًا."
- no_likes_title: "لم بالإعجاب بأي مواضيع حتى الآن"
- no_likes_body: "هناك طريقة رائعة للتقدّم والبدء في المساهمة هي البَدْء في قراءة المحادثات التي جرت بالفعل، واختيار %{heartIcon} على المشاركات التي تريدها."
- no_topics_title: "لم تنشئ أي مواضيع حتى الآن"
- no_read_topics_title: "لم تقرأ أي مواضيع حتى الآن"
- no_read_topics_body: "بمجرد البَدْء في قراءة المناقشات، سترى قائمة هنا. لبدء القراءة، ابحث عن الموضوعات التي تهمك في أعلى أو فئات أو البحث عن طريق الكلمة الرئيسية %{searchIcon}"
- no_group_messages_title: "لم يتم العثور على أي رسالة من المجموعة"
+ no_drafts_body: "لست مستعدًا للنشر؟ سنقوم بحفظ مسودة جديدة تلقائيًا وإدراجها هنا كلما بدأت في إنشاء موضوع أو رد أو رسالة شخصية. اضغط زر الإلغاء لتجاهل المسودة أو حفظها للمتابعة لاحقًا."
+ no_likes_title: "لم تسجِّل إعجابك بأي موضوعات حتى الآن"
+ no_likes_title_others: "لم يسجِّل %{username} إعجابه بأي موضوعات بعد"
+ no_likes_body: "إن البدء في قراءة المحادثات التي جرت بالفعل، وضغط %{heartIcon} على المشاركات التي تريدها، طريقة رائعة للانضمام والبدء في المساهمة."
+ no_topics_title: "لم تنشئ أي موضوعات حتى الآن"
+ no_topics_body: "من الأفضل دائمًا البحث في موضوعات المحادثة الحالية قبل بدء موضوع جديد، ولكن إذا كنت واثقًا من أن الموضوع الذي تريده ليس موجودًا بالفعل، فابدأ موضوعًا جديدًا. ابحث عن الزر + موضوع جديد في الجزء العلوي الأيمن من قائمة الموضوعات أو الفئة أو الوسم للبدء في إنشاء موضوع جديد في تلك المنطقة."
+ no_topics_title_others: "لم يبدأ %{username} أي موضوعات بعد"
+ no_read_topics_title: "لم تقرأ أي موضوعات حتى الآن"
+ no_read_topics_body: "بعد البدء في قراءة المناقشات، سترى قائمة هنا. لبدء القراءة، ابحث عن الموضوعات التي تهمك في الأعلى أو الفئات، أو عن طريق البحث بالكلمة الرئيسية %{searchIcon}"
+ no_group_messages_title: "لم يتم العثور على أي رسائل من المجموعة"
topic_entrance:
- sr_jump_top_button: "انتقل إلى المشاركة الأولى"
- sr_jump_bottom_button: "انتقل إلى آخر مشاركة"
+ sr_jump_top_button: "الانتقال إلى المنشور الأول"
+ sr_jump_bottom_button: "الانتقال إلى المنشور الأخير"
fullscreen_table:
expand_btn: "توسيع الجدول"
second_factor_auth:
- redirect_after_success: "مصادقة العامل الثاني ناجحة. إعادة التوجيه إلى الصفحة السابقة…"
+ redirect_after_success: "نجحت المصادقة الثنائية. جارٍ إعادة التوجيه إلى الصفحة السابقة…"
sidebar:
unread_count:
zero: "%{count} غير مقروء"
@@ -4278,16 +4567,19 @@ ar:
zero: "%{count} جديد"
one: "%{count} جديد"
two: "%{count} جديدان"
- few: "%{count} جديد"
+ few: "%{count} جديدة"
many: "%{count} جديدًا"
other: "%{count} جديدة"
+ toggle_section: "تبديل القسم"
more: "المزيد"
all_categories: "كل الفئات"
+ all_tags: "كل الوسوم"
sections:
about:
header_link_text: "نبذة"
messages:
header_link_text: "الرسائل"
+ header_action_title: "إنشاء رسالة شخصية"
links:
inbox: "صندوق الوارد"
sent: "المُرسَلة"
@@ -4297,9 +4589,17 @@ ar:
unread_with_count: "غير المقروءة (%{count})"
archive: "الأرشيف"
tags:
+ none: "لم تضف أي وسوم."
+ click_to_get_started: "انقر هنا للبدء."
header_link_text: "الوسوم"
+ header_action_title: "تعديل وسوم الشريط الجانبي"
+ configure_defaults: "ضبط الإعدادات الافتراضية"
categories:
+ none: "لم تضف أي فئات."
+ click_to_get_started: "انقر هنا للبدء."
header_link_text: "الفئات"
+ header_action_title: "تعديل فئات الشريط الجانبي"
+ configure_defaults: "ضبط الإعدادات الافتراضية"
community:
header_link_text: "المجتمع"
header_action_title: "إنشاء موضوع جديد"
@@ -4311,23 +4611,38 @@ ar:
badges:
content: "الشارات"
everything:
- content: "كل شىء"
- title: "جميع الموضوعات"
+ content: "كل شيء"
+ title: "كل الموضوعات"
faq:
content: "الأسئلة الشائعة"
tracked:
content: "المتتبَّعة"
- title: "جميع المواضيع المُتَعَقبة"
+ title: "كل الموضوعات المتتبَّعة"
groups:
content: "المجموعات"
title: "كل المجموعات"
users:
content: "المستخدمون"
+ title: "جميع المستخدمين"
my_posts:
content: "منشوراتي"
+ title: "منشوراتي"
+ draft_count:
+ zero: "%{count} مسودة"
+ one: "مسودة واحدة (%{count})"
+ two: "مسودتان (%{count})"
+ few: "%{count} مسودات"
+ many: "%{count} مسودة"
+ other: "%{count} مسودة"
review:
content: "المراجعة"
- title: "المراجعة"
+ title: "مراجعة"
+ pending_count: "بقي %{count}"
+ welcome_topic_banner:
+ title: "إنشاء موضوعك الترحيبي"
+ description: "إن موضوعك الترحيبي هو أول ما يقرأه الأعضاء الجُدد. انظر إليه على أنه \"عرض ترويجي\" أو \"بيان مهمة\". دَع الجميع يعرفون الجمهور المستهدف بهذا المجتمع، وما يتوقعون العثور عليه هنا، وما تريد منهم أن يفعلوه أولًا."
+ button_title: "بدء التعديل"
+ until: "حتى:"
admin_js:
type_to_filter: "اكتب للتصفية..."
admin:
@@ -4465,7 +4780,7 @@ ar:
description: "يمكن للمسؤولين رؤية جميع المجموعات."
members_visibility_levels:
title: "من يمكنه رؤية أعضاء هذه المجموعة؟"
- description: "يمكن للمشرفين رؤية أعضاء كل المجموعات. Flair مرئي لجميع المستخدمين."
+ description: "يمكن للمسؤولين رؤية أعضاء كل المجموعات. الطابع مرئي لجميع المستخدمين."
publish_read_state: "نشر حالة قراءة المجموعة على رسائل المجموعة"
membership:
automatic: تلقائية
@@ -4480,7 +4795,7 @@ ar:
few: "يوجد %{count} مستخدمين لديهم نطاقات البريد الإلكتروني الجديدة وستتم إضافتهم إلى المجموعة."
many: "يوجد %{count} مستخدمًا لديهم نطاقات البريد الإلكتروني الجديدة وستتم إضافته إلى المجموعة."
other: "يوجد %{count} مستخدم لديه نطاقات البريد الإلكتروني الجديدة وستتم إضافته إلى المجموعة."
- automatic_membership_associated_groups: "ستتم إضافة المستخدمين الأعضاء في مجموعة في إحدى الخدمات المدرجة هنا تلقائيًا إلى هذه المجموعة عند تسجيل الدخول باستخدام الخدمة."
+ automatic_membership_associated_groups: "ستتم إضافة المستخدمين الأعضاء في مجموعة لإحدى الخدمات المُدرَجة هنا تلقائيًا إلى هذه المجموعة عند تسجيل الدخول باستخدام الخدمة."
primary_group: "الضبط تلقائيًا كمجموعة أساسية"
name_placeholder: "اسم المجموعة بلا مسافات، على غرار قاعدة اسم المستخدم"
primary: "المجموعة الأساسية"
@@ -4532,7 +4847,7 @@ ar:
no_description: (لا يوجد وصف)
all_api_keys: جميع مفاتيح API
user_mode: مستوى المستخدم
- scope_mode: مجال
+ scope_mode: المجال
impersonate_all_users: انتحال شخصية أي مستخدم
single_user: "مستخدم فردي"
user_placeholder: أدخِل اسم المستخدم
@@ -4548,10 +4863,10 @@ ar:
عند استخدام النطاقات، يمكنك تقييد مفتاح API على مجموعة محدَّدة من نقاط النهاية.
يمكنك أيضًا تحديد المعلمات التي سيتم السماح بها. استخدم الفاصلات للفصل بين القيم المتعددة.
title: النطاقات
- granular: Granular
- read_only: قراءة فقط
- global: عام
- global_description: مفتاح API ليس له قيود ويمكن الوصول إلى جميع endpoints.
+ granular: متعددة المستويات
+ read_only: للقراءة فقط
+ global: عامة
+ global_description: ليس هناك قيود مفروضة على مفتاح API ويمكن الوصول إلى كل نقاط النهاية.
resource: المورد
action: الإجراء
allowed_parameters: المعلمات المسموح بها
@@ -4560,19 +4875,19 @@ ar:
allowed_urls: عناوين URL المسموح بها
descriptions:
global:
- read: تقييد مفتاح API على endpoints للقراءة فقط.
+ read: تقييد مفتاح API على نقاط النهاية المخصَّصة للقراءة فقط.
topics:
read: قراءة موضوع أو منشور محدَّد فيه. يتم دعم RSS أيضًا.
write: إنشاء موضوع جديد أو النشر في موضوع موجود
- update: تحديث الموضوع. غَيّر العنوان والتصنيف والعلامات وما إلى ذلك.
+ update: تحديث الموضوع. غيِّر العنوان والفئة والوسوم، إلى آخره.
read_lists: قراءة قوائم الموضوعات مثل الأكثر نشاطًا، والجديدة، والحديثة، وما إلى ذلك. يتم دعم RSS أيضًا.
posts:
edit: تعديل أي منشور أو منشور معيَّن.
categories:
- list: احصل على قائمة التصنيفات.
- show: الحصول على تصنيف واحد عن طريق الهوية.
+ list: احصل على قائمة بالفئات.
+ show: احصل على فئة واحدة بالمعرِّف.
uploads:
- create: ارفع ملف جديد أو ابدأ الرفع مباشرة لملفات فردية أو متعددة الأجزاء إلى وحدة تخزين خارجية.
+ create: حمِّل ملفًا جديدًا أو ابدأ التحميل المباشر الفردي أو متعدد الأجزاء إلى وحدة تخزين خارجية.
users:
bookmarks: إدراج الإشارات المرجعية للمستخدم. تعرض تذكيرات بالإشارات المرجعية عند استخدام تنسيق ICS.
sync_sso: مزامنة مستخدم باستخدام DiscourseConnect
@@ -4586,13 +4901,18 @@ ar:
email:
receive_emails: ادمج هذا النطاق مع مستقبل البريد لمعالجة الرسائل الإلكترونية الواردة.
badges:
- create: إنشاء شارة جديدة.
- show: الحصول على معلومات حول الشارة.
- update: تحديث الشارة.
- delete: حذف الشارة.
- list_user_badges: قائمة شارات المستخدم.
+ create: أنشئ شارة جديدة.
+ show: الحصول على معلومات بشأن إحدى الشارات.
+ update: تحديث شارة.
+ delete: حذف شارة.
+ list_user_badges: إعداد قائمة بشارات المستخدم.
assign_badge_to_user: تعيين شارة للمستخدم.
revoke_badge_from_user: إلغاء شارة من المستخدم.
+ wordpress:
+ publishing: ضروري لميزات النشر في المكوِّن الإضافي WP Discourse (مطلوب).
+ commenting: ضروري لميزات التعليق في المكوِّن الإضافي WP Discourse.
+ discourse_connect: ضروري لميزات DiscourseConnect في المكوِّن الإضافي WP Discourse.
+ utilities: ضروري إذا كنت تستخدم خدمات المكوِّن الإضافي WP Discourse plugin.
web_hooks:
title: "خطافات الويب"
none: "لا توجد خطافات ويب حاليًا."
@@ -4607,7 +4927,6 @@ ar:
go_back: "العودة إلي القائمة"
payload_url: "عنوان URL للحمولة"
payload_url_placeholder: "https://example.com/postreceive"
- warn_local_payload_url: "يبدو أنك تحاول إعداد خطاف ويب على عنوان URL محلي. قد يتسبَّب تسليم الحدث إلى عنوان محلي في حدوث آثار جانبية أو سلوكيات غير متوقعة. هل تريد المتابعة؟"
secret_invalid: "يجب ألا يحتوي الرمز السري على حروف فارغة."
secret_too_short: "يجب ألا يقل الرمز السري عن 12 حرفًا."
secret_placeholder: "سلسلة اختيارية، تُستخدَم في إنشاء توقيع إلكتروني"
@@ -4651,6 +4970,9 @@ ar:
notification_event:
name: "حدث الإشعار"
details: "عند تلقي المستخدم إشعار في موجزه."
+ user_promoted_event:
+ name: "حدث يروِّج له المستخدم"
+ details: "عندما تتم ترقية المستخدم من مستوى ثقة إلى آخر."
user_badge_event:
name: "حدث منح الشارة"
details: "عند حصول المستخدم على شارة."
@@ -4797,7 +5119,7 @@ ar:
delete_confirm: 'هل تريد بالتأكيد حذف السمة "%{theme_name}"؟'
color: "اللون"
opacity: "الشفافية"
- copy: "نسخة مكررة"
+ copy: "تكرار"
copy_to_clipboard: "نسخ الى الحافظة"
copied_to_clipboard: "تم النسخ إلى الحافظة"
copy_to_clipboard_error: "حدث خطأ في نسخ البيانات إلى الحافظة"
@@ -4897,9 +5219,10 @@ ar:
import_web_advanced: "متقدم..."
import_file_tip: "ملف .tar.gz, .zip, or .dcstyle.json الذي يحتوي على السمة"
is_private: "السمة موجودة في مستودع Git خاص"
+ finish_install: "إنهاء تثبيت السمة"
+ last_attempt: "لم تنتهِ عملية التثبيت، آخر محاولة:"
remote_branch: "اسم الفرع (اختياري)"
public_key: "امنح المفتاح العام التالي إذن الوصول إلى المستودع:"
- public_key_note: "بعد إدخال عنوان URL صالح لمستودع خاص أعلاه، سيتم إنشاء مفتاح SSH وعرضه هنا."
install: "تثبيت"
installed: "مثبَّت"
install_popular: "الرائجة"
@@ -4907,6 +5230,8 @@ ar:
install_git_repo: "من مستودع Git"
install_create: "إنشاء جديد"
duplicate_remote_theme: "مكوِّن السمة \"%{name}\" مثبَّت بالفعل، هل تريد بالتأكيد تثبيت نسخة أخرى؟"
+ force_install: "لا يمكن تثبيت السمة نظرًا لتعذُّر الوصول إلى مستودع Git. هل تريد بالتأكيد مواصلة تثبيته؟"
+ create_placeholder: "إنشاء عنصر نائب"
about_theme: "نبذة"
license: "الترخيص"
version: "الإصدار:"
@@ -4929,6 +5254,7 @@ ar:
has_overwritten_history: "لم يعُد إصدار السمة الحالي موجودًا لأن سجل Git قد تم استبداله إجباريًا."
add: "إضافة"
theme_settings: "إعدادات السمة"
+ overriden_settings_explanation: "يتم تمييز الإعدادات المُستبدَلة بنقطة ولون مميز. لإعادة تعيين تلك الإعدادات إلى القيمة الافتراضية، اضغط على زر إعادة التعيين بجوارها."
no_settings: "لا توجد إعدادات لهذه السمة."
theme_translations: "ترجمات السمة"
empty: "لا توجد عناصر"
@@ -4972,7 +5298,7 @@ ar:
يوصى بشدة بوضع بادئة لأسماء الخصائص لتجنُّب تعارضها مع المكوِّنات الإضافية أو الأساسية.
head_tag:
text: "الرأس"
- title: "HTML الذي سيتم إدراجه قبل رأس الوسم "
+ title: "HTML الذي سيتم إدراجه قبل وسم الرأس"
body_tag:
text: "النص الأساسي"
title: "HTML الذي سيتم إدراجه قبل وسم النص الأساسي"
@@ -5073,7 +5399,7 @@ ar:
time: "الوقت"
user: "المستخدم"
email_type: "نوع البريد الكتروني"
- details_title: "إظهار تفاصيل البريد الإلكتروني"
+ details_title: "عرض تفاصيل البريد الإلكتروني"
to_address: "إلى العنوان"
test_email_address: "عنوان البريد الإلكتروني للاختبار"
send_test: "إرسال رسالة إلكترونية للاختبار"
@@ -5091,6 +5417,7 @@ ar:
last_seen_user: "آخر ظهور لمستخدم:"
no_result: "لم يتم العثور على نتائج للملخص."
reply_key: "مفتاح الرد"
+ post_link_with_smtp: "تفاصيل الإرسال وSMTP"
skipped_reason: "سبب التخطي"
incoming_emails:
from_address: "من"
@@ -5120,6 +5447,7 @@ ar:
address_placeholder: "name@example.com"
type_placeholder: "الملخص، الاشتراك..."
reply_key_placeholder: "مفتاح الرد"
+ smtp_transaction_response_placeholder: "معرِّف SMTP"
moderation_history:
performed_by: "أجراه"
no_results: "لا يتوفَّر سجل للإشراف."
@@ -5306,6 +5634,7 @@ ar:
few: "إظهار %{count} كلمات"
many: "إظهار %{count} كلمة"
other: "إظهار %{count} كلمة"
+ case_sensitive: "(حساس لحالة الأحرف)"
download: تنزيل
clear_all: مسح الكل
clear_all_confirm: "هل تريد بالتأكيد مسح جميع الكلمات المُراقَبة للإجراء %{action}؟"
@@ -5343,6 +5672,8 @@ ar:
exists: "موجودة بالفعل"
upload: "الإضافة من ملف"
upload_successful: "تم التحميل بنجاح. وتمت إضافة الكلمات."
+ case_sensitivity_label: "حساس لحالة الأحرف"
+ case_sensitivity_description: "الكلمات ذات حالة الأحرف المطابقة فقط"
test:
button_label: "اختبار"
modal_title: "%{action}: اختبار الكلمات المُراقَبة"
@@ -5443,6 +5774,7 @@ ar:
suspended: "معلَّق؟"
staged: "مؤقت؟"
show_admin_profile: "مسؤول"
+ manage_user: "إدارة المستخدم"
show_public_profile: "إظهار الملف الشخصي العام"
impersonate: "انتحال"
action_logs: "سجلات الإجراءات"
@@ -5463,7 +5795,7 @@ ar:
reputation: الشهرة
permissions: الأذونات
activity: النشاط
- like_count: الإعجابات الممنوحة/المتلقاة
+ like_count: تسجيلات الإعجاب الممنوحة/المتلقاة
last_100_days: "في آخر 100 يوم"
private_topics_count: موضوع خاص
posts_read_count: منشور مقروء
@@ -5488,20 +5820,20 @@ ar:
anonymize_failed: "حدثت مشكلة في أثناء إخفاء هوية الحساب."
delete: "حذف المستخدم"
delete_posts:
- button: "حذف جميع المنشورات"
+ button: "حذف كل المنشورات"
progress:
title: "تقدُّم حذف المنشورات"
description: "جارٍ حذف المنشورات..."
confirmation:
- title: "حذف جميع المشاركات بواسطة @%{username}"
+ title: "حذف كل المنشورات من @%{username}"
description: |
- هل أنت متأكد من رغبتك في حذف %{post_count} المشاركات من قبل @%{username}؟
+ هل تريد بالتأكيد حذف %{post_count} من منشورات @%{username}؟
لا يمكن التراجع عن هذا الإجراء للتأكيد والمتابعة أكتب: للمتابعة، اكتب: يُرجى اختيار مالك جديد لمحتوى @%{username}. سيتم نقل جميع الموضوعات والمنشورات والرسائل والمحتويات الأخرى التي تم إنشاؤها بواسطة @%{username}. سيتم نقل كل الموضوعات والمنشورات والرسائل والمحتويات الأخرى التي تم إنشاؤها بواسطة @%{username}. Pour mettre votre communauté sur pied rapidement, invitez une liste d'utilisateurs : composez un fichier CSV contenant l'adresse de chaque personne à inviter, en disposant une adresse par ligne. Pour ajouter certaines personnes à des groupes particuliers, ou pour les diriger automatiquement vers un sujet particulier lors de leur première connexion, vous pouvez faire figurer les éléments suivants. Un courriel d'invitation sera envoyé à chaque adresse reprise dans ce fichier CSV, et vous pourrez modifier son contenu après l'avoir envoyé sur le serveur. Un e-mail d'invitation sera envoyé à chaque adresse reprise dans ce fichier CSV, et vous pourrez modifier son contenu après l'avoir envoyé sur le serveur. Vous avez des doutes concernant l'adresse courriel que vous avez utilisée ? Saisissez une adresse courriel et nous vous dirons si elle existe ici. Si vous n'avez plus accès à l'adresse courriel de votre compte, merci de contacter nos responsables serviables. Vous avez des doutes concernant l'adresse e-mail que vous avez utilisée ? Saisissez une adresse e-mail et nous vous dirons si elle existe ici. Si vous n'avez plus accès à l'adresse e-mail de votre compte, merci de contacter nos responsables serviables. コミュニティをすばやく拡大するには、ユーザーのリストを招待します。招待するユーザーのメールアドレスごとに少なくとも 1 行を含む CSV ファイルを準備してください。カンマ区切りの情報は、グループに人を追加したい場合、またはそれらのユーザーが初めてサインインしたときに特定のトピックに移動させる場合に利用できます。 コミュニティーをすばやく拡大するには、ユーザーのリストを招待します。招待するユーザーのメールアドレスごとに少なくとも 1 行を含む CSV ファイルを準備してください。カンマ区切りの情報は、グループに人を追加したい場合、またはそれらのユーザーが初めてサインインしたときに特定のトピックに移動させる場合に利用できます。 アップロードされる CSV ファイルに含まれるメールアドレスに招待が送られ、後で管理することができます。 Nodig een lijst met gebruikers uit om uw community snel op gang te helpen. Bereid een CSV-bestand voor met ten minste één rij per e-mailadres van gebruikers die u wilt uitnodigen. De volgende door komma's gescheiden informatie kan worden verstrekt als u mensen aan groepen wilt toevoegen of ze naar een specifiek topic wilt sturen de eerste keer dat ze zich aanmelden. Elk e-mailadres in uw geüploade CSV-bestand zal een uitnodiging opgestuurd krijgen en u zult deze later kunnen beheren. Nodig een lijst van gebruikers uit om je community snel op gang te helpen. Stel een CSV-bestand op met minimaal één rij per e-mailadres van gebruikers die je wilt uitnodigen. De volgende door komma's gescheiden gegevens kunnen worden verstrekt als je mensen aan groepen wilt toevoegen of ze naar een specifiek topic wilt sturen de eerste keer dat ze zich aanmelden. Elk e-mailadres in je geüploade CSV-bestand ontvangt een uitnodiging die je later kunt beheren. Weet u niet zeker welk e-mailadres u hebt gebruikt? Voer een e-mailadres in en we laten u weten of het hier bestaat. Als u geen toegang meer hebt tot het e-mailadres van uw account, neem dan contact op met onze behulpzame staf. Weet je niet zeker welk e-mailadres je hebt gebruikt? Voer een e-mailadres in, dan laten we je weten of het bestaat hier. Als je geen toegang meer hebt tot het e-mailadres van je account, neem dan contact op met onze behulpzame staf. %{username} %{description}"
invited_to_topic: "%{username} %{description}"
- invitee_accepted: "%{username} heeft uw uitnodiging geaccepteerd"
+ invitee_accepted: "%{username} heeft je uitnodiging geaccepteerd"
moved_post: "%{username} heeft %{description} verplaatst"
linked: "%{username} %{description}"
- granted_badge: "'%{description}' ontvangen"
+ granted_badge: "'%{description}' verdiend"
topic_reminder: "%{username} %{description}"
- watching_first_post: "Nieuw Topic %{description}"
- membership_request_accepted: "Lidmaatschap geaccepteerd in '%{group_name}'"
+ watching_first_post: "Nieuw topic %{description}"
+ membership_request_accepted: "Lidmaatschap geaccepteerd voor '%{group_name}'"
membership_request_consolidated:
- one: "%{count} open lidmaatschapsaanvraag voor '%{group_name}'"
- other: "%{count} open lidmaatschapsaanvragen voor '%{group_name}'"
+ one: "%{count} openstaand lidmaatschapsverzoek voor '%{group_name}'"
+ other: "%{count} openstaande lidmaatschapsverzoeken voor '%{group_name}'"
reaction: "%{username} %{description}"
reaction_2: "%{username}, %{username2} %{description}"
votes_released: "%{description} - voltooid"
dismiss_confirmation:
body:
default:
- one: "Weet u het zeker? U hebt %{count} belangrijke melding."
- other: "Weet u het zeker? U hebt %{count} belangrijke meldingen."
+ one: "Weet je het zeker? Je hebt %{count} belangrijke melding."
+ other: "Weet je het zeker? Je hebt %{count} belangrijke meldingen."
dismiss: "Negeren"
cancel: "Annuleren"
group_message_summary:
- one: "%{count} bericht in uw Postvak IN voor %{group_name}"
- other: "%{count} berichten in uw Postvak IN voor %{group_name}"
+ one: "%{count} bericht in je inbox voor %{group_name}"
+ other: "%{count} berichten in je inbox voor %{group_name}"
popup:
- mentioned: '%{username} heeft u genoemd in ''%{topic}'' - %{site_title}'
- group_mentioned: '%{username} heeft u genoemd in ''%{topic}'' - %{site_title}'
- quoted: '%{username} heeft u geciteerd in ''%{topic}'' - %{site_title}'
- replied: '%{username} heeft op u geantwoord in ''%{topic}'' - %{site_title}'
+ mentioned: '%{username} heeft je genoemd in ''%{topic}'' - %{site_title}'
+ group_mentioned: '%{username} heeft je genoemd in ''%{topic}'' - %{site_title}'
+ quoted: '%{username} heeft je geciteerd in ''%{topic}'' - %{site_title}'
+ replied: '%{username} heeft op je geantwoord in ''%{topic}'' - %{site_title}'
posted: '%{username} heeft een bericht geplaatst in ''%{topic}'' - %{site_title}'
- private_message: '%{username} heeft u een persoonlijk bericht gestuurd in ''%{topic}'' - %{site_title}'
- linked: '%{username} heeft een koppeling naar uw bericht geplaatst vanaf ''%{topic}'' - %{site_title}'
+ private_message: '%{username} heeft je een persoonlijk bericht gestuurd in ''%{topic}'' - %{site_title}'
+ linked: '%{username} heeft een link geplaatst naar je bericht in ''%{topic}'' - %{site_title}'
watching_first_post: '%{username} heeft een nieuw topic gemaakt: ''%{topic}'' - %{site_title}'
confirm_title: "Meldingen ingeschakeld - %{site_title}"
confirm_body: "Gelukt! Meldingen zijn ingeschakeld."
@@ -2068,7 +2067,7 @@ nl:
invitee_accepted: "uitnodiging geaccepteerd"
posted: "nieuw bericht"
moved_post: "bericht verplaatst"
- linked: "gekoppeld"
+ linked: "gelinkt"
bookmark_reminder: "bladwijzerherinnering"
bookmark_reminder_with_name: "bladwijzerherinnering - %{name}"
granted_badge: "badge toegekend"
@@ -2076,15 +2075,15 @@ nl:
group_mentioned: "groep genoemd"
group_message_summary: "nieuwe groepsberichten"
watching_first_post: "nieuw topic"
- topic_reminder: "topic-herinnering"
+ topic_reminder: "topicherinnering"
liked_consolidated: "nieuwe likes"
post_approved: "bericht goedgekeurd"
- membership_request_consolidated: "nieuwe lidmaatschapsaanvragen"
+ membership_request_consolidated: "nieuwe lidmaatschapsverzoeken"
reaction: "nieuwe reactie"
votes_released: "Stem is vrijgegeven"
upload_selector:
uploading: "Uploaden"
- select_file: "Bestand selecteren"
+ select_file: "Selecteer bestand"
default_image_alt_text: afbeelding
search:
sort_by: "Sorteren op"
@@ -2098,39 +2097,39 @@ nl:
too_short: "Uw zoekterm is te kort."
clear_search: "Zoekopdracht wissen"
result_count:
- one: "%{count} resultaat voor%{term}"
- other: "%{count}%{plus} resultaten voor%{term}"
+ one: "%{count} resultaat voor %{term}"
+ other: "%{count}%{plus} resultaten voor %{term}"
title: "Zoeken"
full_page_title: "Zoeken"
no_results: "Geen resultaten gevonden."
- no_more_results: "Geen resultaten meer gevonden."
- post_format: "#%{post_number} door %{username}"
+ no_more_results: "Geen verdere resultaten gevonden."
+ post_format: "#%{post_number} van %{username}"
results_page: "Zoekresultaten voor '%{term}'"
- more_results: "Er zijn meer resultaten. Verfijn uw zoekcriteria."
- cant_find: "Kunt u niet vinden wat u zoekt?"
+ more_results: "Er zijn meer resultaten. Verfijn je zoekcriteria."
+ cant_find: "Kun je niet vinden wat je zoekt?"
start_new_topic: "Misschien een nieuw topic starten?"
- or_search_google: "Of probeer in plaats hiervan te zoeken met Google:"
- search_google: "Probeer in plaats hiervan te zoeken met Google:"
+ or_search_google: "Of probeer in plaats daarvan te zoeken met Google:"
+ search_google: "Probeer in plaats daarvan te zoeken met Google:"
search_google_button: "Google"
search_button: "Zoeken"
categories: "Categorieën"
tags: "Tags"
in: "in"
- in_this_topic: "in dit onderwerp"
+ in_this_topic: "in dit topic"
in_topics_posts: "in alle topics en berichten"
enter_hint: "of druk op Enter"
in_posts_by: "In berichten van @%{username}"
type:
- default: "Onderwerpen/berichten"
+ default: "Topics/berichten"
users: "Gebruikers"
categories: "Categorieën"
categories_and_tags: "Categorieën/tags"
context:
- user: "Berichten van @%{username} doorzoeken"
- category: "De categorie #%{category} doorzoeken"
- tag: "De tag #%{tag} doorzoeken"
- topic: "Dit topic doorzoeken"
- private_messages: "Berichten doorzoeken"
+ user: "Zoeken in berichten van @%{username}"
+ category: "Zoeken in de categorie #%{category}"
+ tag: "Zoeken in de tag #%{tag}"
+ topic: "Zoeken in dit topic"
+ private_messages: "Zoeken in berichten"
tips:
category_tag: "filtert op categorie of tag"
full_search: "start zoeken op volledige pagina"
@@ -2146,14 +2145,14 @@ nl:
label: Met badge
with_tags:
label: Getagd
- aria_label: Filter met tags
+ aria_label: Filteren met tags
filters:
label: Alleen topics/berichten weergeven...
- title: Die alleen met de titel overeenkomen
+ title: Alleen matchen in titel
likes: die ik heb geliket
posted: waarin ik iets heb geplaatst
- created: die ik heb aangemaakt
- watching: die ik in de gaten houd
+ created: die ik heb gemaakt
+ watching: die ik observeer
tracking: die ik volg
private: In mijn berichten
bookmarks: Ik heb een bladwijzer gemaakt
@@ -2192,13 +2191,13 @@ nl:
placeholder: maximaal
aria_label: filteren op maximale weergaven
additional_options:
- label: "Filteren op aantal berichten en onderwerpweergaven"
+ label: "Filteren op aantal berichten en topicweergaven"
hamburger_menu: "menu"
new_item: "nieuw"
go_back: "terug"
not_logged_in_user: "gebruikerspagina met samenvatting van huidige activiteit en voorkeuren"
current_user: "naar uw gebruikerspagina"
- view_all: "Alle %{tab} bekijken"
+ view_all: "alle %{tab} weergeven"
user_menu:
tabs:
replies: "Antwoorden"
@@ -2218,85 +2217,85 @@ nl:
dismiss: "Negeren"
dismiss_read: "Alle ongelezen negeren"
dismiss_read_with_selected:
- one: "%{count} ongelezen onderwerp negeren"
- other: "%{count} ongelezen onderwerpen negeren"
+ one: "%{count} ongelezen topic negeren"
+ other: "%{count} ongelezen topics negeren"
dismiss_button: "Negeren..."
dismiss_button_with_selected:
one: "Negeren (%{count})…"
other: "Negeren (%{count})…"
- dismiss_tooltip: "Alleen nieuwe berichten negeren of het volgen van topics stoppen"
- also_dismiss_topics: "Het volgen van deze topics stoppen, zodat ze nooit meer als ongelezen verschijnen."
- dismiss_new: "Nieuwe berichten negeren"
+ dismiss_tooltip: "Negeer alleen nieuwe berichten of stop het volgen van topics"
+ also_dismiss_topics: "Volgen van deze topics stoppen, zodat ze nooit meer als ongelezen verschijnen voor mij"
+ dismiss_new: "Nieuwe negeren"
dismiss_new_with_selected:
- one: "Negeer nieuw topic (%{count})"
- other: "Negeer nieuwe topics (%{count})"
+ one: "Nieuwe negeren (%{count})"
+ other: "Nieuwe negeren (%{count})"
toggle: "bulkselectie van topics in-/uitschakelen"
actions: "Bulkacties"
close_topics: "Topics sluiten"
archive_topics: "Topics archiveren"
- move_messages_to_inbox: "Naar Postvak IN verplaatsen"
+ move_messages_to_inbox: "Verplaatsen naar inbox"
notification_level: "Meldingen..."
- change_notification_level: "Wijzig meldingsniveau"
+ change_notification_level: "Meldingsniveau wijzigen"
choose_new_category: "Kies de nieuwe categorie voor de topics:"
selected:
- one: "U hebt %{count} topic geselecteerd."
- other: "U hebt %{count} topics geselecteerd."
+ one: "Je hebt %{count} topic geselecteerd."
+ other: "Je hebt %{count} topics geselecteerd."
change_tags: "Tags vervangen"
append_tags: "Tags toevoegen"
choose_new_tags: "Kies nieuwe tags voor deze topics:"
- choose_append_tags: "Kies nieuwe tags om voor deze topics toe te voegen:"
+ choose_append_tags: "Kies nieuwe tags om toe te voegen voor deze topics:"
changed_tags: "De tags van deze topics zijn gewijzigd."
- remove_tags: "Verwijder alle tags"
+ remove_tags: "Alle tags verwijderen"
confirm_remove_tags:
- one: "Alle tags zullen verwijderd worden uit dit topic. Weet u het zeker?"
- other: "Alle tags zullen verwijderd worden uit %{count} topics. Weet u het zeker?"
+ one: "Alle tags worden verwijderd van dit topic. Weet je het zeker?"
+ other: "Alle tags worden verwijderd van %{count} topics. Weet je het zeker?"
progress:
one: "Voortgang: %{count} topic"
other: "Voortgang: %{count} topics"
none:
- unread: "U hebt geen ongelezen topics."
- unseen: "U hebt geen ongelezen topics."
- new: "U hebt geen nieuwe topics."
- read: "U hebt nog geen topics gelezen."
- posted: "U hebt nog niet in een topic gereageerd."
- latest: "U bent helemaal bij!"
- bookmarks: "U hebt nog geen bladwijzers voor topics gemaakt."
+ unread: "Je hebt geen ongelezen topics."
+ unseen: "Je hebt geen ongeziene topics."
+ new: "Je hebt geen nieuwe topics."
+ read: "Je hebt nog geen topics gelezen."
+ posted: "Je hebt nog geen berichten geplaatst in een topic."
+ latest: "Je bent helemaal bij!"
+ bookmarks: "Je hebt nog geen bladwijzers voor topics gemaakt."
category: "Er zijn geen topics in %{category}."
top: "Er zijn geen toptopics."
educate:
- new: ' Hier verschijnen uw nieuwe topics. Standaard worden topics als nieuw beschouwd en verschijnt de indicator als deze in de afgelopen 2 dagen zijn aangemaakt. Bezoek uw voorkeuren om dit te wijzigen. Uw ongelezen topics verschijnen hier. Standaard worden topics als ongelezen beschouwd en zullen ze het aantal ongelezen berichten laten zien 1 als u: Of als u het topic expliciet heeft ingesteld op \"Gevolgd\" of \"In de gaten gehouden\" via de \U0001F514 in elk topic. Bezoek uw voorkeuren om dit te wijzigen. Hier worden je nieuwe topics weergegeven. Standaard worden topics als nieuw beschouwd en wordt de indicator weergegeven als ze de afgelopen 2 dagen zijn gemaakt. Ga naar je voorkeuren om dit te wijzigen. Je ongelezen topics worden hier weergegeven. Standaard worden topics als ongelezen beschouwd en wordt het aantal ongelezen berichten aangegeven 1 als je: Of als je het topic expliciet hebt ingesteld op \"Gevolgd\" of \"Geobserveerd\" via de \U0001F514 in elk topic. Ga naar je voorkeuren om dit te wijzigen. Kies een nieuwe eigenaar voor de inhoud van @%{username}. Alle topics, berichten en andere inhoud die door @%{username} is gemaakt, wordt overgedragen. Alle topics, berichten en andere inhoud gemaakt door @%{username} worden overgedragen. بخلاف ذلك، يُرجى إعادة ضبط كلمة المرور. هذه الدعوة إلى %{site_name} لم يعد من الممكن استردادها. يرجى أن تطلب من الشخص الذي دعاك أن يرسل لك دعوة جديدة. لم يعُد من الممكن استرداد هذه الدعوة إلى %{site_name}. يُرجى أن تطلب من الشخص الذي دعاك أن يُرسل إليك دعوة جديدة. لقد أعدنا إرسال الرسالة الإلكترونية للتنشيط إلى %{email}"
safe_mode:
title: "الدخول إلى الوضع الآمن"
+ description: "يسمح لك الوضع الآمن باختبار موقعك دون تحميل المكوِّنات الإضافية أو السمات."
+ no_themes: "إيقاف السمات ومكوِّنات السمات"
no_unofficial_plugins: "إيقاف المكوِّنات الإضافية غير الرسمية"
no_plugins: "إيقاف كل المكوِّنات الإضافية"
enter: "الدخول إلى الوضع الآمن"
@@ -4320,24 +4569,40 @@ ar:
title: "إعداد Discourse"
step:
introduction:
+ title: "أخبرنا عن مجتمعك"
fields:
title:
+ label: "اسم المجتمع"
placeholder: "استراحة جين"
site_description:
+ label: "صِف مجتمعك في جملة واحدة"
placeholder: "مكان لجين وأصدقائها لمناقشة أشياء رائعة"
+ contact_email:
+ label: "نقطة الاتصال"
+ placeholder: "example@user.com"
+ description: "الشخص أو المجموعة المسؤولة عن هذا المجتمع. تُستخدَم للتحديثات المهمة، ومُدرَج في صفحة نبذة عنك للتواصل العاجل."
default_locale:
label: "اللغة"
privacy:
+ title: "تجربة العضو"
fields:
login_required:
placeholder: "خاصة"
extra_description: "يمكن للمستخدمين المسجَّلين فقط الوصول إلى هذا المجتمع"
+ invite_only:
+ placeholder: "الدعوة فقط"
+ extra_description: "يجب دعوة المستخدمين من قِبل المستخدمين الموثوقين أو فريق العمل، وإلا فسيتمكن المستخدمون من التسجيل بأنفسهم."
must_approve_users:
placeholder: "طلب الموافقة"
+ extra_description: "يجب الموافقة على المستخدمين من قِبل فريق العمل"
ready:
title: "تم إعداد Discourse!"
+ description: "هذا كل شيء! لقد انتهيت من الخطوات الأساسية لإعداد مجتمعك. يمكنك البدء الآن وإلقاء نظرة، وكتابة موضوع ترحيبي، وإرسال الدعوات! Votre invitation à %{site_name} a déjà été utilisée. La date d'expiration de cette invitation à %{site_name} est dépassée. Nous vous invitons à demander à la personne qui vous a invité(e) de vous faire parvenir une nouvelle invitation. Vous y êtes presque ! Nous avons envoyé un courriel d'activation à votre adresse. Merci de suivre les instructions figurant dans le courriel pour activer votre compte. Si vous ne le recevez pas, veuillez vérifier votre dossier de courrier indésirable. Vous y êtes presque ! Nous avons envoyé un e-mail d'activation à votre adresse. Merci de suivre les instructions figurant dans l'e-mail pour activer votre compte. Si vous ne le recevez pas, veuillez vérifier votre dossier de courrier indésirable. C'est presque terminé ! Nous avons envoyé un courriel d'activation à %{email}. Merci de suivre les instructions qui y figurent pour activer votre compte. Si vous ne recevez pas ce courriel, vérifiez le dossier du courrier indésirable dans votre messagerie. C'est presque terminé ! Nous avons envoyé un e-mail d'activation à %{email}. Merci de suivre les instructions qui y figurent pour activer votre compte. Si vous ne recevez pas cet e-mail, vérifiez le dossier du courrier indésirable dans votre messagerie. Nous avons envoyé un courriel d'activation à %{email}. Veuillez suivre les instructions qui y figurent pour activer votre compte. Si vous ne le recevez pas, vérifiez votre dossier de courrier indésirable et assurez-vous de configurer correctement le courriel. Nous avons envoyé un e-mail d'activation à %{email}. Veuillez suivre les instructions qui y figurent pour activer votre compte. Si vous ne le recevez pas, vérifiez votre dossier de courrier indésirable et assurez-vous de configurer correctement l'e-mail. Nous avons renvoyé le courriel d'activation à %{email}"
+ title: "Renvoyer l'e-mail d'activation"
+ message: " Nous avons renvoyé l'e-mail d'activation à %{email}"
safe_mode:
title: "Activer le mode sans échec"
description: "Le mode sans échec vous permet de tester votre site sans charger d'extension ni de thème."
@@ -4634,7 +4631,7 @@ fr:
category: "Les messages de cette catégorie doivent être approuvés manuellement par un responsable. Voir %{link}."
must_approve_users: "Tous les nouveaux utilisateurs doivent être approuvés par un responsable. Voir %{link}."
invite_only: "Tous les nouveaux utilisateurs doivent être invités. Voir %{link}."
- email_auth_res_enqueue: "Ce courriel n'a pas passé une vérification DMARC, il est fort probable qu'il ne provienne pas de l'expéditeur annoncé. Vérifiez les en-têtes du courriel brut pour plus d'informations."
+ email_auth_res_enqueue: "Cet e-mail n'a pas passé une vérification DMARC, il est fort probable qu'il ne provienne pas de l'expéditeur annoncé. Vérifiez les en-têtes de l'e-mail brut pour obtenir plus d'informations."
email_spam: "Cette adresse e-mail a été identifiée comme un courrier indésirable d'après l'en-tête défini dans %{link}."
suspect_user: "Ce nouvel utilisateur a saisi les informations de son profil sans lire aucun sujet ou article, ce qui suggère fortement qu'il s'agit peut-être d'un spammeur. Voir %{link}."
contains_media: "Ce message contient un ou plusieurs contenus multimédias ou vidéo. Voir %{link}."
@@ -4704,7 +4701,7 @@ fr:
description: "L'utilisateur sera supprimé du forum."
block:
title: "Supprimer et bloquer l'utilisateur"
- description: "L'utilisateur sera supprimé et nous bloquerons ses adresses courriel et IP."
+ description: "L'utilisateur sera supprimé et nous bloquerons ses adresses e-mail et IP."
reject:
title: "Rejeter"
bundle_title: "Refuser…"
diff --git a/config/locales/server.gl.yml b/config/locales/server.gl.yml
index 6cb09ef809..5860ba242d 100644
--- a/config/locales/server.gl.yml
+++ b/config/locales/server.gl.yml
@@ -690,7 +690,6 @@ gl:
one: "hai case %{count} ano"
other: "hai case %{count} anos"
password_reset:
- no_token: "Sentímolo, esa ligazón de cambio de contrasinal é antiga de máis. Prema no botón Iniciar sesión e déalle a «Esquecín o meu contrasinal» para obter unha nova ligazón."
choose_new: "Elixir un novo contrasinal"
choose: "Elixir un contrasinal"
update: "Actualizar o contrasinal"
diff --git a/config/locales/server.he.yml b/config/locales/server.he.yml
index 326efdd2fb..b25618f846 100644
--- a/config/locales/server.he.yml
+++ b/config/locales/server.he.yml
@@ -266,6 +266,7 @@ he:
max_redemptions_allowed_one: "להזמנות דוא״ל זה אמור להיות 1."
redemption_count_less_than_max: "אמור להיות קטן מ־%{max_redemptions_allowed}."
email_xor_domain: "שדות הדוא״ל ושם התחום אסורים לשימוש בו־זמנית"
+ existing_user_success: "ההזמנה נוצלה בהצלחה"
bulk_invite:
file_should_be_csv: "הקובץ שנשלח אמור להיות בתסדיר csv."
max_rows: "%{max_bulk_invites} ההזמנות הראשונות נשלחו. כדאי לנסות לפצל את הקובץ לחלקים קטנים יותר."
@@ -545,7 +546,7 @@ he:
ניתן לערוך תגובות קודמות שלך כדי להוסיף ציטוט על ידי סימון טקסט ובחירה בכפתור שנגלה ציטוט תגובה.
יותר קל לכולם לקרוא נושאים שיש בהם פחות תגובות עמוקות מאשר הרבה תגובות קטנות.
- dominating_topic: פרסמת כאן למעלה מ־%{percent}% מהתגובות, האם כדאי לשמוע דעות של אנשים נוספים?
+ dominating_topic: פרסמת כאן יותר מ־%{percent}% מהתגובות; נוכל להציע לך לתת לאנשים אחרים הזדמנות להשתתף?
get_a_room: הגבת ל־@%{reply_username} %{count} פעמים, ידעת שניתן לשלוח הודעה פרטית במקום?
too_many_replies: |
### הגעת למספר התגובות המרבי לנושא זה
@@ -866,7 +867,7 @@ he:
many: "לפני %{count} שנים כמעט"
other: "לפני %{count} שנים כמעט"
password_reset:
- no_token: "הקישור להחלפת הסיסמה ישן מדי, עמך הסליחה. נא לבחור בכפתור הכניסה ולהשתמש באפשרות ‚שכחתי את הסיסמה שלי’ כדי לקבל קישור חדש.י"
+ no_token: 'אוי ויי! הקישור בו השתמשת לא פעיל עוד. אפשר להיכנס כעת. אם שכחת את הסיסמה שלך, אפשר לבקש קישור כדי לאפס אותה.'
choose_new: "נא לבחור בסיסמה חדשה"
choose: "בחירת סיסמה"
update: "עדכון סיסמה"
@@ -2939,6 +2940,12 @@ he:
bulk_invite_succeeded:
title: "הזמנה קבוצתית הצליחה"
subject_template: "ההזמנה הקבוצתית עובדה בהצלחה"
+ text_body_template: |
+ ההזמנה המרוכזת שלך עברה עיבוד, %{sent} הזמנות נשלחו בדוא״ל, %{skipped} דולגו ו־%{warnings} אזהרות.
+
+ ``` text
+ %{logs}
+ ```
bulk_invite_failed:
title: "הזמנה קבוצתית נכשלה"
subject_template: "ההזמנה המרוכזת עובדה עם שגיאות"
@@ -3835,7 +3842,7 @@ he:
png_to_jpg_conversion_failure_message: "אירעה שגיאה בעת המרה מ־PNG ל־JPG."
optimize_failure_message: "אירעה שגיאה בעת מיטוב התמונה שהועלתה."
download_failure: "הורדת הקובץ מהספק החיצוני נכשלה."
- size_mismatch_failure: "גודל הקובץ שנשלח ל־S3 לא תואם לגודל המשלוח המוצהר. %{additional_detail}"
+ size_mismatch_failure: "גודל הקובץ שנשלח ל־S3 לא תואם לגודל המיועד למשלוח כלפי חוץ. %{additional_detail}"
create_multipart_failure: "יצירת העלאה מרובת חלקים באחסון החיצוני נכשלה."
abort_multipart_failure: "ביטול העלאה מרובת חלקים באחסון החיצוני נכשל."
complete_multipart_failure: "השלמת העלאה מרובת חלקים באחסון החיצוני נכשלה."
@@ -4904,6 +4911,10 @@ he:
user_status:
errors:
ends_at_should_be_greater_than_set_at: "ends_at אמור להיות גדול מאשר set_at"
+ webhooks:
+ payload_url:
+ blocked_or_internal: "לא ניתן להשתמש בכתובת המטען כי היא תואמת ל־IP חסום או פנימי"
+ unsafe: "לא ניתן להשתמש בכתובת המטען כיוון שהיא לא מאובטחת"
activemodel:
errors:
<<: *errors
diff --git a/config/locales/server.hu.yml b/config/locales/server.hu.yml
index bcdd5d53ee..db1284bd8d 100644
--- a/config/locales/server.hu.yml
+++ b/config/locales/server.hu.yml
@@ -487,7 +487,6 @@ hu:
one: "majdnem %{count} éve"
other: "majdnem %{count} éve"
password_reset:
- no_token: "Ez a jelszómódosítási hivatkozás túl régi. Ahhoz, hogy új hivatkozást kapjon, válassza a Bejelentkezés gombot, és használja az „Elfelejtettem a jelszót” lehetőséget."
choose_new: "Válasszon új jelszót"
choose: "Válasszon jelszót"
update: "Jelszó frissítése"
diff --git a/config/locales/server.hy.yml b/config/locales/server.hy.yml
index 13b1f83bed..c57cf80eea 100644
--- a/config/locales/server.hy.yml
+++ b/config/locales/server.hy.yml
@@ -534,7 +534,6 @@ hy:
one: "գրեթե %{count} տարի առաջ"
other: "գրեթե %{count} տարի առաջ"
password_reset:
- no_token: "Ներողություն, գաղտնաբառի փոփոխության այդ հղումը շատ հին է: Ընտրեք Մուտք Գործել կոճակը և օգտագործեք 'Ես մոռացել եմ իմ գաղտնաբառը'՝ նոր հղում ստանալու համար:"
choose_new: "Ընտրել նոր գաղտնաբառ"
choose: "Ընտրել գաղտնաբառ"
update: "Թարմացնել Գաղտնաբառը"
diff --git a/config/locales/server.it.yml b/config/locales/server.it.yml
index de6d95d78c..5d0d0ff850 100644
--- a/config/locales/server.it.yml
+++ b/config/locales/server.it.yml
@@ -495,7 +495,6 @@ it:
Puoi modificare la risposta precedente per aggiungere una citazione evidenziando il testo e selezionando il pulsante Cita che comparirà.
E' più facile per chiunque leggere argomenti che hanno poche risposte ma più dettagliate, rispetto a molte brevi risposte singole.
- dominating_topic: Hai pubblicato più del %{percent}% delle risposte qui; lascia esprimere anche qualcun altro.
get_a_room: Abbiamo notato che hai già risposto %{count} volte a @%{reply_username}. Sapevi che puoi anche scambiare messaggi personali con questa persona?
too_many_replies: |
### Hai raggiungo il limite di risposte per questo argomento
@@ -764,7 +763,6 @@ it:
one: "quasi %{count} anno fa"
other: "quasi %{count} anni fa"
password_reset:
- no_token: "Spiacenti, il collegamento per il cambio password è scaduto. Clicca su Accedi e poi su 'Ho dimenticato la password' per ricevere un nuovo collegamento."
choose_new: "Scegli una nuova password"
choose: "Scegli una password"
update: "Aggiorna Password"
@@ -3554,7 +3552,6 @@ it:
png_to_jpg_conversion_failure_message: "Si è verificato un errore durante la conversione da PNG a JPG."
optimize_failure_message: "Si è verificato un errore nell'ottimizzare l'immagine caricata."
download_failure: "Il download del file dal provider esterno non è riuscito."
- size_mismatch_failure: "La dimensione del file caricato su S3 non corrispondeva alla dimensione prevista per lo stub di caricamento esterno. %{additional_detail}"
create_multipart_failure: "Errore nella creazione del caricamento multipart nell'archivio esterno."
abort_multipart_failure: "Errore nell’annullamento del caricamento multipart nell'archivio esterno."
complete_multipart_failure: "Impossibile completare il caricamento multipart nell'archivio esterno."
diff --git a/config/locales/server.ja.yml b/config/locales/server.ja.yml
index 384a791e71..6861426bbf 100644
--- a/config/locales/server.ja.yml
+++ b/config/locales/server.ja.yml
@@ -443,25 +443,25 @@ ja:
- 一般的に使用されている言葉をトピックに含めると、ほかのユーザーが*検索*しやすくなります。トピックを関連トピックとグループ化するには、カテゴリ (またはタグ) を選択してください。
- 詳細については、[コミュニティガイドラインをご覧ください](%{base_path}/guidelines)。このパネルは最初の%{education_posts_text}にのみ表示されます。
+ 詳細については、[コミュニティーガイドラインをご覧ください](%{base_path}/guidelines)。このパネルは最初の%{education_posts_text}にのみ表示されます。
"new-reply": |
%{site_name} へようこそ — **貢献していただきありがとうございます!**
- - 他のコミュニティメンバーに親切に接してください。
+ - 他のコミュニティーメンバーに親切に接してください。
‐ あなたの返信によって会話が改善されますか?
‐ 建設的な批判は歓迎されますが、ユーザーではなく*アイデア*を批判してください。
- 詳細は、[コミュニティガイドラインをご覧ください](%{base_path}/guidelines)。このパネルは最初の%{education_posts_text}にのみ表示されます。
+ 詳細は、[コミュニティーガイドラインをご覧ください](%{base_path}/guidelines)。このパネルは最初の%{education_posts_text}にのみ表示されます。
avatar: |
### アカウントに写真はいかがですか?
- トピックと返信をいくつか投稿していますが、プロフィール写真が単なる文字になっています。
+ トピックと返信をいくつか投稿していますが、プロファイル写真が単なる文字になっています。
- **[ユーザープロフィールにアクセス](%{profile_path})**して、あなたを表す写真をアップロードしませんか?
+ **[ユーザープロファイルにアクセス](%{profile_path})**して、あなたを表す写真をアップロードしませんか?
- 全員が特有のプロフィール写真を設定していれば、会話のディスカッションをフォローしやすく、面白い人たちを見つけやすくなります!
+ 全員が特有のプロファイル写真を設定していれば、会話のディスカッションをフォローしやすく、面白い人たちを見つけやすくなります!
sequential_replies: |
### 複数の投稿にまとめて返信しましょう
@@ -470,7 +470,6 @@ ja:
前の返信を編集する際に、テキストをハイライトして引用して返信ボタンを選択すると、返信に引用を追加することができます。
短い個別の返信を多数読むよりも、詳細な返信を数件読む方が、トピックをフォローしやすくなります。
- dominating_topic: 返信の %{percent}% 以上はあなたが投稿したものです。他にあなたが意見を聞きたい人はいませんか?
get_a_room: '@%{reply_username} に %{count} 回返信しましたが、個人メッセージを送信できることをご存知ですか?'
too_many_replies: |
### このトピックへの返信数の制限に達しました
@@ -530,7 +529,7 @@ ja:
user_profile:
attributes:
featured_topic_id:
- invalid: "このトピックをプロフィールに掲載することはできません。"
+ invalid: "このトピックをプロファイルに掲載することはできません。"
user_email:
attributes:
user_id:
@@ -581,14 +580,14 @@ ja:
staff_category_name: "スタッフ"
staff_category_description: "スタッフディスカッション用の非公開カテゴリです。トピックは管理者とモデレーターのみが閲覧できます。"
discourse_welcome_topic:
- title: "コミュニティへようこそ!"
+ title: "コミュニティーへようこそ!"
body: |2
この固定トピックの最初の写真は、ようこそメッセージとしてホームページを初めて訪問するすべてのユーザーに表示されるものであるため、重要です!
- コミュニティの簡単な説明になるように、**これを編集**しましょう。
+ コミュニティーの簡単な説明になるように、**これを編集**しましょう。
- - これは誰のためのコミュニティですか?
+ - これは誰のためのコミュニティーですか?
- どのような情報を得られますか?
- 参加するメリットは何ですか?
- 詳細 (リンク、リソースなど) はどこに記載されていますか?
@@ -643,8 +642,8 @@ ja:
slow_down: "この操作の実行回数が多すぎます。後でもう一度お試しください。"
too_many_requests: "この操作の実行回数が多すぎます。%{time_left}経ってからもう一度お試しください。"
by_type:
- first_day_replies_per_day: "あなたの熱意には感謝していますが、コミュニティの安全上の理由により、新規ユーザーが初日に作成できる返信の上限に達しました。%{time_left}経ってから、返信を作成してください。"
- first_day_topics_per_day: "あなたの熱意には感謝していますが、コミュニティの安全上の理由により、新規ユーザーが初日に作成できるトピックの上限に達しました。%{time_left}経ってから、新しいトピックを作成してください。"
+ first_day_replies_per_day: "あなたの熱意には感謝していますが、コミュニティーの安全上の理由により、新規ユーザーが初日に作成できる返信の上限に達しました。%{time_left}経ってから、返信を作成してください。"
+ first_day_topics_per_day: "あなたの熱意には感謝していますが、コミュニティーの安全上の理由により、新規ユーザーが初日に作成できるトピックの上限に達しました。%{time_left}経ってから、新しいトピックを作成してください。"
create_topic: "トピックの作成に急ぎ過ぎているようです。%{time_left}経ってから、もう一度お試しください。"
create_post: "返信に急ぎ過ぎているようです。%{time_left}経ってから、もう一度お試しください。"
delete_post: "投稿の削除に急ぎ過ぎているようです。%{time_left}経ってから、もう一度お試しください。"
@@ -713,7 +712,6 @@ ja:
almost_x_years:
other: "ほぼ %{count} 年前"
password_reset:
- no_token: "パスワード変更用のリンクは古すぎます。ログインボタンを選択し、「パスワードを忘れました」を使って新しいリンクを取得してください。"
choose_new: "新しいパスワードを選択する"
choose: "パスワードを選択する"
update: "パスワードを更新"
@@ -809,8 +807,8 @@ ja:
email_body: "%{link}\n\n%{message}"
inappropriate:
title: "不適切"
- description: 'この投稿には一般的な人が攻撃的、虐待的、またはコミュニティガイドラインに違反すると見なすコンテンツが含まれています。'
- short_description: 'コミュニティガイドラインの違反'
+ description: 'この投稿には一般的な人が攻撃的、虐待的、またはコミュニティーガイドラインに違反すると見なすコンテンツが含まれています。'
+ short_description: 'コミュニティーガイドラインの違反'
notify_user:
title: "@%{username} にメッセージを送る"
description: "この投稿についてこのユーザーと直接話すことを希望します。"
@@ -875,9 +873,9 @@ ja:
short_description: "これは広告です"
inappropriate:
title: "不適切"
- description: 'このトピックには一般的な人が攻撃的、虐待的、またはコミュニティガイドラインに違反すると見なすコンテンツが含まれています。'
+ description: 'このトピックには一般的な人が攻撃的、虐待的、またはコミュニティーガイドラインに違反すると見なすコンテンツが含まれています。'
long_form: "不適切として通報"
- short_description: 'コミュニティガイドラインの違反'
+ short_description: 'コミュニティーガイドラインの違反'
notify_moderators:
title: "その他"
description: 'このトピックはガイドライン、利用規約または上記に記載されていない別の理由で、一般のスタッフによる注意が必要です。'
@@ -886,8 +884,8 @@ ja:
email_title: 'トピック "%{title}" はモデレーターの注意が必要です'
email_body: "%{link}\n\n%{message}"
flagging:
- you_must_edit: ' あなたの投稿はコミュニティから通報されました。投稿したメッセージを確認してください。 この投稿はコミュニティから通報されたため、一時的に非表示にされています。 あなたの投稿はコミュニティーから通報されました。投稿したメッセージを確認してください。 この投稿はコミュニティーから通報されたため、一時的に非表示にされています。 無視されたコンテンツ これらの設定を変更することがある場合は、いつでもこのウィザードを実行するか、あなたの管理者セクションにアクセスしてください。このセクションはサイトメニューのレンチアイコンの横にあります。 強力なテーマシステムを使用すると、Discourse をさらにカスタマイズすることができます。たとえば、meta.discourse.org で人気のテーマやコンポーネントをご覧ください。 それでは、新しいコミュニティの構築をお楽しみください! それでは、新しいコミュニティーの構築をお楽しみください! Uw bericht is gemarkeerd door de gemeenschap. Bekijk uw berichten. Uw bericht is gemarkeerd door de gemeenschap en is tijdelijk verborgen. Uw bericht is gemarkeerd door de community. Bekijk uw berichten. Uw bericht is gemarkeerd door de community en is tijdelijk verborgen. Verborgen inhoud U bent er bijna! We hebben een activeringsmail naar %{email} gestuurd. Volg de instructies in het e-mailbericht om uw account te activeren. Als dit niet aankomt, contoleer dan uw spammap. We hebben de activeringsmail opnieuw naar %{email} verstuurd"
+ message: " We hebben de activerings-e-mail opnieuw naar %{email} gestuurd"
safe_mode:
title: "Veilige modus starten"
no_unofficial_plugins: "Niet-officiële plug-ins uitschakelen"
@@ -2842,7 +2839,7 @@ nl:
fields:
login_required:
placeholder: "Privé"
- extra_description: "Alleen aangemelde gebruikers hebben toegang tot deze gemeenschap"
+ extra_description: "Alleen aangemelde gebruikers hebben toegang tot deze community"
must_approve_users:
placeholder: "Heeft goedkeuring nodig"
ready:
@@ -2895,7 +2892,7 @@ nl:
label: "Geautomatiseerde berichten"
invites:
title: "Staf uitnodigen"
- description: "U bent bijna klaar! Nodig wat mensen uit om te helpen met het seeden van uw discussies met interessante topics en berichten om uw gemeenschap op te zetten."
+ description: "U bent bijna klaar! Nodig wat mensen uit om te helpen met het seeden van uw discussies met interessante topics en berichten om uw community op te zetten."
disabled: "Omdat sociale aanmeldingen zijn uitgeschakeld, is het niet mogelijk om uitnodigingen naar iedereen te versturen. Ga verder met de volgende stap."
finished:
title: "Uw Discourse is gereed!"
@@ -2910,7 +2907,7 @@ nl:
replied: '%{username} heeft op u geantwoord in ''%{topic}'' - %{site_title}'
posted: '%{username} heeft een bericht geplaatst in ''%{topic}'' - %{site_title}'
private_message: '%{username} heeft u een privébericht gestuurd in ''%{topic}'' - %{site_title}'
- linked: '%{username} heeft een koppeling naar uw bericht geplaatst vanaf ''%{topic}'' - %{site_title}'
+ linked: '%{username} heeft een link naar je bericht geplaatst vanaf ''%{topic}'' - %{site_title}'
watching_first_post: '%{username} heeft een nieuw topic gemaakt: ''%{topic}'' - %{site_title}'
confirm_title: "Meldingen ingeschakeld - %{site_title}"
confirm_body: "Gelukt! Meldingen zijn ingeschakeld."
@@ -2936,8 +2933,8 @@ nl:
low: "Laag"
medium: "Gemiddeld"
high: "Hoog"
- must_claim: "U moet items opeisen voordat u er handelingen op kunt uitvoeren."
- user_claimed: "Dit item is door een andere gebruiker opgeëist."
+ must_claim: "U moet items claimen voordat u er acties op kunt uitvoeren."
+ user_claimed: "Dit item is door een andere gebruiker geclaimd."
missing_version: "U moet een versieparameter opgeven"
conflict: "Een updateconflict heeft ervoor gezorgd dat u dit niet kon doen."
reasons:
@@ -2973,14 +2970,14 @@ nl:
delete_and_ignore_replies:
title: "Bericht + antwoorden verwijderen en negeren"
description: "Bericht en alle antwoorden erop verwijderen; als dit het eerste bericht is, ook het topic verwijderen"
- confirm: "Weet u zeker dat u de antwoorden op het bericht ook wilt verwijderen?"
+ confirm: "Weet je zeker dat je de antwoorden op het bericht ook wilt verwijderen?"
delete_and_agree:
title: "Bericht verwijderen en akkoord"
description: "Bericht verwijderen; als dit het eerste bericht is, ook het topic verwijderen"
delete_and_agree_replies:
title: "Bericht + antwoorden verwijderen en akkoord"
description: "Bericht en alle antwoorden erop verwijderen; als dit het eerste bericht is, ook het topic verwijderen"
- confirm: "Weet u zeker dat u de antwoorden op het bericht ook wilt verwijderen?"
+ confirm: "Weet je zeker dat je de antwoorden op het bericht ook wilt verwijderen?"
disagree_and_restore:
title: "Niet akkoord en bericht terugzetten"
description: "Het bericht terugzetten, zodat alle gebruikers het kunnen zien."
diff --git a/config/locales/server.pl_PL.yml b/config/locales/server.pl_PL.yml
index 942d0c98f6..60019ff0fe 100644
--- a/config/locales/server.pl_PL.yml
+++ b/config/locales/server.pl_PL.yml
@@ -260,6 +260,7 @@ pl_PL:
invalid_access: "Nie masz uprawnień do przeglądania żądanego zasobu."
requires_groups: "Zaproszenie nie zostało zapisane, ponieważ podany temat jest niedostępny. Dodaj jedną z następujących grup: %{groups}."
domain_not_allowed: "Twój adres e-mail nie może zostać użyty do zrealizowania tego zaproszenia."
+ existing_user_success: "Zaproszenie zostało pomyślnie zrealizowane"
bulk_invite:
file_should_be_csv: "Wgrywany plik powinien być formatu csv."
max_rows: "Wysłano %{max_bulk_invites} pierwszych zaproszeń. Sprobuj podzielić plik na mniejsze części."
@@ -538,7 +539,7 @@ pl_PL:
Możesz edytować swoje wcześniejsze odpowiedzi, aby dodać cytat, poprzez podświetlenie tekstu i wybranie przycisku cytuj odpowiedź który się pojawi.
Każdemu łatwiej będzie przeczytać temat, który ma kilka pogłębionych odpowiedzi, zamiast wielu niewielkich, indywidualnych.
- dominating_topic: Zamieściłeś tutaj ponad %{percent}% odpowiedzi, czy jest ktoś jeszcze, z kim chciałbyś się skontaktować?
+ dominating_topic: Zamieściłeś tutaj ponad %{percent}% odpowiedzi; czy możemy zasugerować, żebyś dał innym ludziom możliwość wypowiedzenia się?
get_a_room: Odpowiedziałeś na @%{reply_username} %{count} razy. Czy wiesz, że możesz zamiast tego wysłać wiadomość osobistą?
too_many_replies: |
### Osiągnąłeś/łaś limit odpowiedzi w tym temacie
@@ -643,6 +644,7 @@ pl_PL:
<<: *errors
uncategorized_category_name: "Bez kategorii"
general_category_name: "Ogólne"
+ general_category_description: "Utwórz tutaj tematy, które nie pasują do żadnej innej istniejącej kategorii."
meta_category_name: "Dyskusje o serwisie"
meta_category_description: "Dyskusje o tej stronie, jej organizacji, tym jak działa i jak możemy ją usprawnić."
staff_category_name: "Zespół"
@@ -858,7 +860,6 @@ pl_PL:
many: "prawie %{count} lat temu"
other: "prawie %{count} lat temu"
password_reset:
- no_token: "Przepraszamy, ten link do zmiany hasła jest zbyt stary. Wybierz przycisk \"Zaloguj\", a następnie \"Zapomniałem hasła\", by uzyskać nowy link."
choose_new: "Wprowadź nowe hasło"
choose: "Wprowadź hasło"
update: "Aktualizuj hasło"
@@ -1637,6 +1638,7 @@ pl_PL:
post_menu: "Określ które elementy menu wpisu powinny być widoczne i w jakiej kolejności. Przykład like|edit|flag|delete|share|bookmark|reply"
post_menu_hidden_items: "Elementy w menu, które w menu wpisu będą ukryte, chyba że kliknięty zostanie przycisk rozwijania."
share_links: "Określ które elementy menu udostępniania powinny być widoczne i w jakiej kolejności. "
+ allow_username_in_share_links: "Zezwalaj na umieszczanie nazw użytkowników w linkach do udostępniania. Jest to przydatne, aby nagradzać odznaki na podstawie unikalnych odwiedzających."
site_contact_username: "Nazwa użytkownika, spod której wysyłane będą automatyczne wiadomości. Jeśli pusta, użyte będzie domyślne konto System."
site_contact_group_name: "Prawidłowa nazwa grupy, do której zostaną zaproszone wszystkie automatyczne wiadomości."
send_welcome_message: "Wyślij wszystkim nowym użytkownikom powitalną wiadomość z krótkim przewodnikiem."
@@ -1659,6 +1661,7 @@ pl_PL:
enable_badges: "Włącz system odznak"
max_favorite_badges: "Maksymalna liczba odznak, które użytkownik może wybrać"
enable_whispers: "Pozwalaj administracji na prywatną rozmowę w tematach."
+ whispers_allowed_groups: "Zezwalaj na prywatną komunikację w ramach tematów członkom określonych grup."
allow_index_in_robots_txt: "W pliku robots.txt określ, że ta witryna może być indeksowana przez wyszukiwarki internetowe. W wyjątkowych przypadkach możesz trwale zastąpić plik robots.txt."
blocked_email_domains: "Lista domen poczty e-mail rozdzielonych pionową kreską, z których użytkownicy nie mogą rejestrować kont. Przykład: mailinator.com|trashmail.net"
allowed_email_domains: "Lista rozdzielonych pionową kreską domen e-mail, z których użytkownicy MUSZĄ się rejestrować. UWAGA: Użytkownicy z domenami e-mail innymi niż wypisane tutaj nie będą mogli się zarejestrować!"
@@ -1709,6 +1712,7 @@ pl_PL:
google_oauth2_client_secret: "Client Secret twojej aplikacji w Google"
google_oauth2_prompt: "Opcjonalna lista wartości łańcuchowych rozdzielanych spacjami, która określa, czy serwer autoryzacji monituje użytkownika o ponowne uwierzytelnienie i zgodę. Możliwe wartości można znaleźć na https://developers.google.com/identity/protocols/OpenIDConnect#prompt ."
google_oauth2_hd: "Opcjonalna domena Google Apps Hosted, do której logowanie będzie ograniczone. Więcej informacji można znaleźć na https://developers.google.com/identity/protocols/OpenIDConnect#hd-param"
+ google_oauth2_hd_groups: "(eksperymentalne) Pobierz grupy dyskusyjne Google użytkowników w domenie hostowanej po uwierzytelnieniu. Pobrane grupy dyskusyjne Google mogą być używane do przyznawania automatycznego członkostwa w grupach Discourse (zobacz ustawienia grupy). Więcej informacji znajdziesz na https://meta.discourse.org/t/226850"
enable_twitter_logins: "Włącz uwierzytelnianie przez Twittera, wymaga twitter_consumer_key i twitter_consumer_secret. Zobacz Konfigurowanie logowania przez Twittera (i bogatych embedów) dla Discourse."
twitter_consumer_key: "Klucz klienta do uwierzytelnienia na Twitterze, zarejestrowany na https://developer.twitter.com/apps"
twitter_consumer_secret: "Sekret klienta dotyczący uwierzytelniania na Twitterze, zarejestrowany na stronie https://developer.twitter.com/apps"
@@ -3794,7 +3798,6 @@ pl_PL:
png_to_jpg_conversion_failure_message: "Wystąpił błąd podczas konwersji z PNG na JPG."
optimize_failure_message: "Wystąpił błąd podczas optymalizacji przesłanego obrazu."
download_failure: "Pobieranie pliku od zewnętrznego dostawcy nie powiodło się."
- size_mismatch_failure: "Rozmiar pliku przesłanego do S3 nie był zgodny z rozmiarem przewidzianym przez zewnętrzny serwer przesyłania. %{additional_detail}"
create_multipart_failure: "Nie udało się utworzyć wieloczęściowego przesyłania w zewnętrznym sklepie."
checksum_mismatch_failure: "Suma kontrolna przesłanego pliku nie zgadza się. Zawartość pliku mogła ulec zmianie podczas przesyłania. Proszę spróbuj ponownie."
cannot_promote_failure: "Przesyłanie nie może zostać zakończone, być może zostało już zakończone lub wcześniej zakończone niepowodzeniem."
diff --git a/config/locales/server.pt.yml b/config/locales/server.pt.yml
index 5ec1cbd01f..f9072b7dc3 100644
--- a/config/locales/server.pt.yml
+++ b/config/locales/server.pt.yml
@@ -591,7 +591,6 @@ pt:
one: "há quase %{count} ano atrás"
other: "há quase %{count} anos atrás"
password_reset:
- no_token: "Desculpe, essa hiperligação para alterar a palavra-passe é muito antiga. Selecione o botão Iniciar Sessão e utilize 'Esqueci a minha palavra-passe' para obter uma nova hiperligação."
choose_new: "Escolha uma nova palavra-passe"
choose: "Escolha uma palavra-passe"
update: "Atualizar Palavra-passe"
diff --git a/config/locales/server.pt_BR.yml b/config/locales/server.pt_BR.yml
index c9c99fddb2..8d4cb70964 100644
--- a/config/locales/server.pt_BR.yml
+++ b/config/locales/server.pt_BR.yml
@@ -71,7 +71,7 @@ pt_BR:
file_too_big: "O arquivo não compactado é muito grande."
unknown_file_type: "O arquivo que você enviou não parece ser um tema válido do Discourse."
not_allowed_theme: "\"%{repo}\" não está na lista de temas permitidos (confira a configuração global \"allowed_theme_repos\")."
- ssh_key_gone: "Você esperou muito tempo para instalar o tema e a chave SSH expirou. Por favor, tente novamente."
+ ssh_key_gone: "Você esperou muito tempo para instalar o tema e a chave SSH expirou. Tente novamente."
errors:
component_no_user_selectable: "Componentes do tema não podem ser selecionados pelo(a) usuário(a)"
component_no_default: "Componentes do tema não podem ser o tema padrão"
@@ -495,7 +495,6 @@ pt_BR:
Você pode editar sua resposta anterior para adicionar uma citação selecionando um texto e clicando no botão responder citação que aparecerá.
Fica mais fácil para todos(as) lerem tópicos com poucas respostas e muitas informações em vez de muitas respostas individuais e curtas.
- dominating_topic: Você postou mais do que %{percent} das respostas aqui, deseja saber a opinião de outra pessoa?
get_a_room: Você respondeu a %{reply_username} %{count} vezes, sabia que poderia ter enviado uma mensagem pessoal em vez de fazer isso?
too_many_replies: |
### Você atingiu o limite de respostas para este tópico
@@ -678,7 +677,7 @@ pt_BR:
public_group_membership: "Você está entrando/saindo de grupos com frequência. Aguarde %{time_left} antes de tentar novamente."
topics_per_day: "Você alcançou a quantidade máxima de novos tópicos permitidos por dia. Crie mais tópicos novos em %{time_left}."
pms_per_day: "Você alcançou a quantidade máxima de mensagens novas permitidas por dia. Crie mais mensagens novas em %{time_left}."
- create_like: "Uau! Você tem compartilhado muito amor! Você atingiu o número máximo de curtidas em um período de 24 horas, mas à medida que ganha níveis de confiança, você ganha mais curtidas diárias. Você poderá curtir as postagens novamente em %{time_left}."
+ create_like: "Uau! Você tem compartilhado muito amor! Você atingiu o número máximo de curtidas em um período de 24 horas. Mas, à medida que ganha níveis de confiança, você ganha mais curtidas diárias. Você poderá curtir as postagens novamente em %{time_left}."
create_bookmark: "Você alcançou a quantidade máxima de favoritos por dia. Crie mais favoritos em %{time_left}."
edit_post: "Você alcançou a quantidade máxima de edições por dia. Envie mais edições em %{time_left}."
live_post_counts: "Você está pedindo contagens de postagens em tempo real muito rápido. Espere %{time_left} antes de tentar novamente."
@@ -764,7 +763,6 @@ pt_BR:
one: "há quase %{count} ano"
other: "há quase %{count} anos"
password_reset:
- no_token: "Desculpe, este link de alteração de senha é muito antigo. Selecione o botão Entrar e use \"Esqueci minha senha\" para obter um novo link."
choose_new: "Escolha uma nova senha"
choose: "Escolha uma senha"
update: "Atualizar senha"
@@ -1335,7 +1333,7 @@ pt_BR:
labels:
user: Usuário(a)
qtt_like: Curtidas recebidas
- description: "Melhores 10 usuários(as) que receberram curtidas."
+ description: "Melhores dez usuários(as) que receberram curtidas."
top_users_by_likes_received_from_inferior_trust_level:
title: "Melhores usuários(as) por curtidas recebidas de usuário(a) com nível de confiança mais baixo"
labels:
@@ -1348,7 +1346,7 @@ pt_BR:
labels:
user: Usuário(a)
qtt_like: Curtidas recebidas
- description: "Melhores 10 usuários(as) que receberam curtidas de uma ampla variedade de pessoas."
+ description: "Melhores dez usuários(as) que receberam curtidas de uma ampla variedade de pessoas."
dashboard:
group_email_credentials_warning: 'Houve um problema nas credenciais do e-mail do grupo %{group_full_name}. Nenhum e-mail será enviado a partir da caixa de entrada do grupo até este problema ser resolvido. %{error}'
rails_env_warning: "Seu servidor está rodando no modo %{env}."
@@ -1424,7 +1422,7 @@ pt_BR:
download_remote_images_threshold: "Espaço mínimo necessário em disco para baixar em modo local imagens remotas (em %)"
disabled_image_download_domains: "Imagens remotas hospedadas nestes domínios nunca serão baixadas. Lista delimitada por barras verticais."
block_hotlinked_media: "Impedir que os usuários introduzam mídia remota (hotlinked) em suas publicações. A mídia remota que não for baixada via 'download_remote_images_to_local' será substituída por um link de espaço reservado."
- block_hotlinked_media_exceptions: "Uma lista de URLs base, isentos da configuração block_hotlinked_media. Inclua o protocolo (por exemplo, https://example.com)."
+ block_hotlinked_media_exceptions: "Uma lista de URLs de base, isentos da configuração block_hotlinked_media. Inclua o protocolo (por exemplo, https://example.com)."
editing_grace_period: "Durante (n) segundos após a publicação, as edições não criarão uma nova versão no histórico da postagem."
editing_grace_period_max_diff: "Quantidade máxima de alterações de caracteres permitidas no período de carência de edição. Se houver mais alterações, armazene outra revisão de postagem (nível de confiança 0 e 1)"
editing_grace_period_max_diff_high_trust: "Quantidade máxima de alterações de caracteres permitidas no período de carência de edição. Se houver mais alterações, armazene outra revisão de postagem (nível de confiança 2 e maior)"
@@ -1447,7 +1445,7 @@ pt_BR:
show_pinned_excerpt_desktop: "Mostrar trecho de tópicos fixados na visualização para desktop."
post_onebox_maxlength: "Tamanho máximo para uma postagem do Discourse no Onebox."
blocked_onebox_domains: "Uma lista de domínios que nunca serão \"oneboxed\", por exemplo, wikipedia.org\n(símbolos coringas como \"*\" e \"?\" não são suportados)"
- block_onebox_on_redirect: "Bloqueie o onebox para URLs que redirecionam."
+ block_onebox_on_redirect: "Bloqueie o Onebox para URLs que redirecionam."
allowed_inline_onebox_domains: "Uma lista de domínios que serão colocados em miniatura no Onebox se forem vinculados sem um título"
enable_inline_onebox_on_all_domains: "Ignore a configuração do site inline_onebox_domain_whitelist e permita inclusões de Onebox em todos os domínios."
force_custom_user_agent_hosts: "Hosts para os quais usar um agente do(a) usuário(a) do Onebox em todos os pedidos. (Especialmente útil para hosts que limitam acesso do agente do(a) usuário(a).)"
@@ -3687,7 +3685,6 @@ pt_BR:
png_to_jpg_conversion_failure_message: "Ocorreu um erro ao converter PNG em JPG."
optimize_failure_message: "Ocorreu um erro ao otimizar a imagem enviada."
download_failure: "Falha ao baixar o arquivo do provedor externo."
- size_mismatch_failure: "O tamanho do arquivo enviado para S3 não corresponde ao tamanho pretendido do stub do envio externo. %{additional_detail}"
create_multipart_failure: "Falha ao criar envio com partes múltiplas no armazenamento externo."
abort_multipart_failure: "Falha ao anular envio com partes múltiplas no armazenamento externo."
complete_multipart_failure: "Falha ao concluir envio com partes múltiplas no armazenamento externo."
@@ -3750,12 +3747,12 @@ pt_BR:
dark_rose: "Rosa-escuro"
wcag: "WCAG claro"
wcag_theme_name: "WCAG claro"
- dracula: "Dracula"
- dracula_theme_name: "Dracula"
- solarized_light: "Solarized Light"
- solarized_light_theme_name: "Solarized Light"
- solarized_dark: "Solarized Dark"
- solarized_dark_theme_name: "Solarized Dark"
+ dracula: "Drácula"
+ dracula_theme_name: "Drácula"
+ solarized_light: "Cor clara solarizada"
+ solarized_light_theme_name: "Cor clara solarizada"
+ solarized_dark: "Cor escura solarizada"
+ solarized_dark_theme_name: "Cor escura solarizada"
wcag_dark: "WCAG escuro"
wcag_dark_theme_name: "WCAG escuro"
default_theme_name: "Padrão"
@@ -4410,11 +4407,11 @@ pt_BR:
synonym: 'Sinônimos não são permitidos. Use "%{tag_name}".'
has_synonyms: '"%{tag_name}" não pode ser usado porque tem sinônimos.'
restricted_tags_cannot_be_used_in_category:
- one: 'A etiqueta "%{tags}" não pode ser usada na categoria "%{category}". Por favor, remova-a.'
- other: 'As seguintes etiquetas não podem ser usadas na categoria "%{category}": %{tags}. Por favor, remova-as.'
+ one: 'A etiqueta "%{tags}" não pode ser usada na categoria "%{category}". Remova.'
+ other: 'As seguintes etiquetas não podem ser usadas na categoria "%{category}": %{tags}. Remova.'
category_does_not_allow_tags:
- one: 'A categoria "%{category}" não permite o uso da etiqueta "%{tags}". Por favor, remova-a.'
- other: 'A categoria "%{category}" não permite o uso das etiquetas "%{tags}". Por favor, remova-as.'
+ one: 'A categoria "%{category}" não permite o uso da etiqueta "%{tags}". Remova.'
+ other: 'A categoria "%{category}" não permite o uso das etiquetas "%{tags}". Remova.'
required_tags_from_group:
one: "Você deve incluir pelo menos %{count} etiqueta %{tag_group_name}. As etiquetas deste grupo são: %{tags}."
other: "Você deve incluir pelo menos %{count} etiquetas %{tag_group_name}. As etiquetas deste grupo são: %{tags}."
@@ -4468,7 +4465,7 @@ pt_BR:
extra_description: "Somente usuários(as) que entraram com a conta podem acessar esta comunidade"
invite_only:
placeholder: "Apenas Convidados"
- extra_description: "Usuários(as) devem ser convidados(as) por usuários(as) confiáveis ou pela equipe, caso contrário, poderão se cadastrar por conta própria"
+ extra_description: "Usuários(as) devem ser convidados(as) por usuários(as) confiáveis ou pela equipe. Caso contrário, poderão se cadastrar por conta própria"
must_approve_users:
placeholder: "Requerer aprovação"
extra_description: "Os usuários devem ser aprovados pela equipe"
@@ -4476,7 +4473,7 @@ pt_BR:
title: "Seu Discourse está pronto!"
description: "É isso! Você fez o básico para configurar sua comunidade. Agora você pode entrar, dar uma olhada, escrever um tópico de boas-vindas e enviar convites! #{uploads.first.original_filename}
+
+ <%=
+ select_tag :chat_email_frequency,
+ options_for_select(@chat_email_frequencies, @current_chat_email_frequency),
+ class: 'combobox'
+ %>
+ {{html-safe (i18n "chat.incoming_webhooks.instructions")}} {{i18n "chat.empty_state.direct_message"}} {{this.instructionsText}}
+ {{this.instructionsText}}
+
+ {{this.channel.description}}
+
+ {{i18n "chat.settings.followed"}}
+ {{this.instructions}}
+ {{d-icon "upload"}}
+ Drop a file to upload it.
+
+ {{i18n "chat.emoji_picker.no_results"}}
+ {{this.instructionsText}} {{this.mentionedCannotSeeText}}
+ {{this.mentionedWithoutMembershipText}}
+
+ {{i18n "chat.mention_warning.invite"}}
+
+ {{this.newTopicInstruction}} {{this.existingTopicInstruction}} {{this.newMessageInstruction}}
+ {{i18n "chat.direct_message_creator.no_results"}}
+
+ {{html-safe (i18n
+ "user_menu.no_chat_notifications_body"
+ preferencesUrl=(get-url "/my/preferences/notifications")
+ )}}
+ تؤدي أرشفة القناة إلى وضعها في وضع القراءة فقط ونقل جميع الرسائل من القناة إلى موضوع جديد أو موجود. لا يمكن إرسال رسائل جديدة، ولا يمكن تعديل أو حذف أي رسائل حالية. هل أنت متأكد أنك تريد أرشفة قناة %{channelTitle}؟ يحذف قناة %{name} وسجل الدردشة. سيتم حذف جميع الرسائل والبيانات ذات الصلة، مثل التفاعلات والتحميلات نهائيًا. إذا كنت ترغب في الحفاظ على سجل القناة وإيقاف تشغيلها، فقد ترغب في أرشفة القناة بدلًا من ذلك. هل تريد بالتأكيد المتابعة إلى الحذف النهائي للقناة؟ للتأكيد، اكتب اسم القناة في المربع أدناه. Das Archivieren eines Kanals versetzt ihn in den schreibgeschützten Modus und verschiebt alle Nachrichten aus dem Kanal in ein neues oder vorhandenes Thema. Es können keine neuen Nachrichten gesendet und keine bestehenden Nachrichten bearbeitet oder gelöscht werden. Möchtest du den Kanal %{channelTitle} wirklich archivieren? Löscht den Kanal %{name} und den Chat-Verlauf. Alle Nachrichten und zugehörigen Daten wie Reaktionen und Uploads werden dauerhaft gelöscht. Wenn du den Kanalverlauf beibehalten und den Kanal nur außer Betrieb nehmen möchtest, kannst du ihn stattdessen archivieren. Möchtest du den Kanal wirklich dauerhaft löschen? Gib zur Bestätigung den Namen des Kanals in das Feld unten ein. Archiving a channel puts it into read-only mode and moves all messages from the channel into a new or existing topic. No new messages can be sent, and no existing messages can be edited or deleted. Are you sure you want to archive the %{channelTitle} channel? Deletes the %{name} channel and chat history. All messages and related data, such as reactions and uploads, will be permanently deleted. If you want to preserve the channel history and decomission it, you may want to archive the channel instead. Are you sure you want to permanently delete the channel? To confirm, type the name of the channel in the box below. Archivar un canal lo pone en modo de solo lectura y mueve todos los mensajes del canal a un tema nuevo o existente. No se pueden enviar mensajes nuevos y no se pueden editar ni eliminar mensajes existentes. ¿Seguro que desea archivar el canal %{channelTitle} ? Elimina el canal de %{name} y el historial de chat. Todos los mensajes y datos relacionados, como las reacciones y las subidas, se eliminarán permanentemente. Si quieres conservar el historial del canal y descomponerlo, quizá quieras archivar el canal en su lugar. ¿Seguro que quieres eliminar permanentemente el canal? Para confirmarlo, escribe el nombre del canal en la casilla de abajo. Kanavan arkistointi asettaa sen vain luku -tilaan ja siirtää kaikki kanavan viestit uuteen tai olemassa olevaan ketjuun. Uusia viestejä ei voi lähettää, eikä olemassa olevia viestejä voi muokata tai poistaa. Oletko varma, että haluat arkistoida kanavan %{channelTitle}? Poistaa kanavan %{name} ja chat-historian. Kaikki viestit ja niihin liittyvät tiedot, kuten reaktiot ja lataukset, poistetaan pysyvästi. Jos haluat säilyttää kanavan historian ja poistaa sen käytöstä, voit sen sijaan arkistoida kanavan. Haluatko varmasti poistaa kanavan psyyvästi? Vahvista kirjoittamalla kanavan nimi alla olevaan ruutuun. L'archivage d'un canal le place en mode lecture seule et déplace tous les messages du canal vers un sujet nouveau ou existant. Aucun nouveau message ne peut être envoyé et aucun message existant ne peut être modifié ou supprimé. Voulez-vous vraiment archiver le canal %{channelTitle} ? Supprime le canal %{name} et l'historique des discussions. Tous les messages et données associées, telles que les réactions et les téléversements, seront définitivement supprimés. Si vous souhaitez conserver l'historique du canal et le désactiver, vous pouvez plutôt archiver le canal. Voulez-vous vraiment supprimer définitivement le canal ? Pour confirmer, saisissez le nom du canal dans la case ci-dessous. העברת ערוץ לארכיון מעבירה אותו למצב לקריאה בלבד ומעביר את כל ההודעות מהערוץ לנושא חדש או קיים. אי אפשר לשלוח הודעות חדשות ואי אפשר לערוך או למחוק הודעות קיימות. להעביר את הערוץ %{channelTitle} לארכיון? תהליך זה ימחק את הערוץ %{name} ואת היסטוריית ההתכתבות בו. כל ההודעות והנתונים הקשורים כגון תגובות והעלאות יימחקו לחלוטין. אם עדיף לך לשמר את היסטוריית הערוץ ולבטל אותו, אפשר להעביר את הערוץ לארכיון במקום. למחוק את הערוץ לצמיתות? כדי לאשר, נא למלא את שם הערוץ בתיבה שלהלן. Arhiviranje kanala stavlja ga u način rada samo za čitanje i premješta sve poruke s kanala u novu ili postojeću temu. Ne mogu se slati nove poruke, niti se postojeće poruke ne mogu uređivati ili brisati. Jeste li sigurni da želite arhivirati %{channelTitle} kanal? Briše %{name} kanal i povijest razgovora. Sve poruke i srodni podaci, kao što su reakcije i prijenosi, trajno će biti izbrisani. Ako želite sačuvati povijest kanala i raspada ga, možda želite arhivirati kanal umjesto. Jeste li sigurni da želite trajno izbrisati kanal? Da biste potvrdili, upišite naziv kanala u okvir ispod. Egy csatorna archiválása írásvédett módba helyezi azt, és a csatorna összes üzenetét egy új vagy meglévő témába helyezi át. Új üzeneteket nem lehet küldeni, és a meglévő üzeneteket nem lehet szerkeszteni vagy törölni. Biztos, hogy archiválja a(z) %{channelTitle} csatornát? Törli a(z) %{name} csatornát és a csevegési előzményeket. Minden üzenet és a kapcsolódó adat, például a reakciók és a feltöltések véglegesen törlődnek. Ha meg szeretné őrizni a csatorna előzményeit, de meg akarja szüntetni, akkor inkább archiválja a csatornát. Biztos, hogy véglegesen törli a csatornát? A megerősítéshez írja be a csatorna nevét az alábbi mezőbe. L'archiviazione di un canale lo mette in modalità di sola lettura e sposta tutti i messaggi dal canale a un argomento nuovo o esistente. Non è possibile inviare nuovi messaggi e non è possibile modificare o eliminare messaggi esistenti. Sei sicuro di voler archiviare il canale %{channelTitle}? Elimina il canale %{name} e la cronologia delle chat. Tutti i messaggi e i dati correlati, come reazioni e caricamenti, verranno eliminati in modo permanente. Se vuoi conservare la cronologia del canale e rimuoverla, puoi invece archiviare il canale. Sei sicuro di voler eliminare definitivamente il canale? Per confermare, digita il nome del canale nella casella sottostante. チャンネルをアーカイブすると、チャンネルは読み取り専用モードになり、すべてのメッセージが新しいトピックまたは既存のトピックに移動されます。新しいメッセージを送信することはできません。また既存のメッセージの編集や削除も行えません。 %{channelTitle} チャンネルをアーカイブしてもよろしいですか? %{name} チャンネルとチャット履歴を削除します。すべてのメッセージと、リアクションやアップロードといった関連データは永久に削除されます。チャンネルの履歴を保持して閉鎖するには、チャンネルを削除ではなくアーカイブすることをお勧めします。 チャンネルを永久に削除してもよろしいですか?確定するには、チャンネルの名前を下のボックスに入力してください。 O arquivamento de um canal o coloca em modo somente leitura e move todas as mensagens do canal para um tópico novo ou existente. Nenhuma mensagem nova pode ser enviada e nenhuma mensagem existente pode ser editada ou excluída. Tem certeza de que deseja arquivar o canal %{channelTitle}? Exclui o canal %{name} e o histórico do chat. Todas as mensagens e dados relacionados, como reações e envios, serão excluídos permanentemente. Se você quiser preservar o histórico do canal e desativá-lo, talvez seja melhor arquivar o canal. Tem certeza de que deseja excluir permanentemente o canal? Para confirmar, digite o nome do canal na caixa abaixo. Архивация канала переводит его в режим только для чтения и перемещает все сообщения из канала в новую или существующую тему. В этом режиме нельзя отправлять новые сообщения, а существующие сообщения нельзя редактировать или удалять. Вы действительно хотите заархивировать канал %{channelTitle}? Удаление канала %{name} и истории чата. Все сообщения и связанные с ними данные, такие как эмодзи и загрузки, будут безвозвратно удалены. Если вы не хотите использовать канал, при этом сохранив его историю, вы можете его заархивировать. Вы действительно хотите навсегда удалить канал? Для подтверждения введите название канала в расположенное ниже поле. Arkivering av en kanal sätter den i skrivskyddat läge och flyttar alla meddelanden från kanalen till ett nytt eller existerande ämne. Inga nya meddelanden kan skickas och inga befintliga meddelanden kan redigeras eller raderas. Är du säker på att du vill arkivera kanalen %{channelTitle}? Tar bort %{name} kanalen och chatthistoriken. Alla meddelanden och relaterad data, såsom reaktioner och uppladdningar, kommer att raderas permanent. Om du vill bevara kanalhistoriken men avveckla den, kanske du vill arkivera kanalen istället. Är du säker på att du permanent vill ta bort kanalen? För att bekräfta, skriv in namnet på kanalen i rutan nedan. 归档频道会使它进入只读模式,并将该频道的所有消息移至一个新的或现有的话题。将无法发送新消息,也不能编辑或删除现有消息。 确定要归档%{channelTitle}频道吗? 删除%{name}频道和聊天历史。所有消息和相关数据,例如回应和上传,将被永久删除。如果您想保留频道历史记录,并将其停用,您可能想要归档频道。 确定要永久删除该频道吗?要确认,请在下面的框中输入频道的名称。 This is a chat message. This is a chat message. This is a chat message. This is a chat message. This is a chat message. This is an inline onebox https://en.wikipedia.org/wiki/Hyperlink. <h1>test</h1> ## heading 2 --- a quote this is the first message and another cool one bold @mention ■■■■■ https://github.com/discourse/discourse-chat/pull/468 wow look it's a list this is a replace test the planet of the apes was earth all along <h1>@#{user.username}</h1> Hello world! hi hi hi hi hello! hellloooo%{text}%{text}
Jos tarvitset apua, voit lähettää viestin henkilökunnan jäsenelle.
+ Haluatko suoraa henkilökohtaista keskustelua jonkun kanssa normaalin keskusteluvirran ulkopuolella? Lähetä hänelle viesti klikkaamalla hänen profiilikuvaansa ja käyttämällä viestipainiketta %{icon}.
Jos tarvitset apua, voit lähettää viestin henkilökunnan jäsenelle.
no_bookmarks_title: "Et ole vielä lisännyt mitään kirjanmerkkeihin"
no_bookmarks_body: >
Aloita kirjanmerkkien lisääminen painikkeella %{icon} ja ne listataan tähän helposti löydettäväksi. Voit ajastaa myös muistutuksen!
@@ -1078,7 +1078,7 @@ fi:
Saat tässä paneelissa ilmoituksen muunlaisesta toiminnasta, joka voi olla sinulle merkityksellistä – esimerkiksi kun joku linkittää johonkin viesteistäsi tai muokkaa jotakin niistä.
no_notifications_page_title: "Sinulla ei ole vielä ilmoituksia"
no_notifications_page_body: >
- Saat ilmoituksia sinulle relevantista toiminnasta, kuten omien ketjujesi viesteistä ja vastauksista, kun joku @mainitsee nimesi tai lainaa sinua tai vastaa tarkkailemiisi ketjuihin. Ilmoitukset lähetetään sinulle sähköpostitse silloin, kun et ole kirjautunut hetkeen.
Napsauta painiketta %{icon}, kun haluat ilmoituksia tietystä ketjusta, alueista tai tunnisteista. Lisää valintoja löydät ilmoitusasetuksista.
+ Saat ilmoituksia sinulle relevantista toiminnasta, kuten omien ketjujesi viesteistä ja vastauksista, kun joku @mainitsee nimesi tai lainaa sinua tai vastaa tarkkailemiisi ketjuihin. Ilmoitukset lähetetään sinulle sähköpostitse silloin, kun et ole kirjautunut hetkeen.
Klikkaa painiketta %{icon}, kun haluat ilmoituksia tietystä ketjusta, alueista tai tunnisteista. Lisää valintoja löydät ilmoitusasetuksista.
first_notification: "Ensimmäinen ilmoitus sinulle! Valitse se aloittaaksesi."
dynamic_favicon: "Näytä määrät selaimen kuvakkeessa"
skip_new_user_tips:
@@ -1113,9 +1113,6 @@ fi:
tags_section: "Tunnisteet-osio"
tags_section_instruction: "Valitut tunnisteet näkyvät sivupalkin tunnisteet-osiossa."
navigation_section: "Navigointi"
- list_destination_instruction: "Kun napsautan sivupalkin ketjuluettelon linkkiä, jossa on uusia tai lukemattomia ketjuja, siirry kohtaan"
- list_destination_default: "Oletus"
- list_destination_unread_new: "Uudet/lukemattomat"
change: "vaihda"
featured_topic: "Valikoitu ketju"
moderator: "%{user} on valvoja"
@@ -2006,7 +2003,7 @@ fi:
drafts_offline: "offline-luonnokset"
edit_conflict: "muokkauskonflikti"
esc: "esc"
- esc_label: "Kuittaa napsauttamalla tai painamalla Esc-näppäintä"
+ esc_label: "Kuittaa klikkaamalla tai painamalla Esc-näppäintä"
ok_proceed: "Ok, jatka"
group_mentioned_limit:
one: "Varoitus! Olet maininnut ryhmän %{group}, mutta ryhmässä on yli %{count} käyttäjä, mikä on ylläpitäjän asettama rajoitus maininnoille. Kukaan ei saa ilmoitusta."
@@ -2425,8 +2422,8 @@ fi:
other: "Kirjanmerkit – %{count} lukematonta kirjanmerkkiä"
review_queue: "Tarkastusjono"
review_queue_with_unread:
- one: "Tarkistusjono – %{count} kohde vaatii tarkistuksen"
- other: "Tarkistusjono – %{count} kohdetta vaatii tarkistuksen"
+ one: "Tarkastusjono – %{count} kohde vaatii tarkistuksen"
+ other: "Tarkastusjono – %{count} kohdetta vaatii tarkistuksen"
other_notifications: "Muut ilmoitukset"
other_notifications_with_unread:
one: "Muut ilmoitukset – %{count} lukematon ilmoitus"
@@ -2941,8 +2938,8 @@ fi:
sr_collapse_replies: "Kutista upotetut vastaukset"
sr_date: "Viestin päivämäärä"
sr_expand_replies:
- one: "Tällä viestillä on %{count} vastaus. Laajenna napsauttamalla."
- other: "Tällä viestillä on %{count} vastausta. Laajenna napsauttamalla."
+ one: "Tällä viestillä on %{count} vastaus. Laajenna klikkaamalla."
+ other: "Tällä viestillä on %{count} vastausta. Laajenna klikkaamalla."
expand_collapse: "laajenna/kutista"
sr_below_embedded_posts_description: "viestin %{post_number} vastaukset"
sr_embedded_reply_description: "käyttäjän %{username} vastaus viestiin %{post_number}"
@@ -3600,7 +3597,7 @@ fi:
edit: "%{shortcut} Muokkaa viestiä"
delete: "%{shortcut} Poista viesti"
mark_muted: "%{shortcut} Vaimenna ketju"
- mark_regular: "%{shortcut} Normaali ketju (oletus)"
+ mark_regular: "%{shortcut} Tavallinen ketju (oletus)"
mark_tracking: "%{shortcut} Seuraa ketjua"
mark_watching: "%{shortcut} Tarkkaile ketjua"
print: "%{shortcut} Tulosta ketju"
@@ -3868,13 +3865,13 @@ fi:
archive: "Arkisto"
tags:
none: "Et ole lisännyt tunnisteita."
- click_to_get_started: "Aloita napsauttamalla tätä."
+ click_to_get_started: "Aloita klikkaamalla tätä."
header_link_text: "Tunnisteet"
header_action_title: "muokkaa sivupalkin tunnisteitasi"
configure_defaults: "Määritä oletukset"
categories:
none: "Et ole lisännyt alueita."
- click_to_get_started: "Aloita napsauttamalla tätä."
+ click_to_get_started: "Aloita klikkaamalla tätä."
header_link_text: "Alueet"
header_action_title: "muokkaa sivupalkin alueitasi"
configure_defaults: "Määritä oletukset"
@@ -3913,8 +3910,8 @@ fi:
title: "käsittele"
pending_count: "%{count} odottaa"
welcome_topic_banner:
- title: "Luo tervetuloaiheesi"
- description: 'Tervetuloaiheesi on ensimmäinen asia, jonka uudet jäsenet lukevat. Ajattele sitä "hissipuheenasi" tai "tavoitelausumanasi". Kerro kaikille, kenelle tämä yhteisö on tarkoitettu, mitä he voivat odottaa löytävänsä täältä ja mitä haluat heidän tekevän ensin.'
+ title: "Luo tervetuloketjusi"
+ description: "Tervetuloketjusi on ensimmäinen asia, jonka uudet jäsenet lukevat. Ajattele sitä \"hissipuheenasi\" tai \"tavoitelausumanasi\". Kerro kaikille, kenelle tämä yhteisö on tarkoitettu, mitä he voivat odottaa löytävänsä täältä ja mitä haluat heidän tekevän ensin."
button_title: "Aloita muokkaaminen"
until: "Asti:"
admin_js:
@@ -4188,7 +4185,6 @@ fi:
go_back: "Takaisin luetteloon"
payload_url: "Tietosisällön URL"
payload_url_placeholder: "https://esimerkki.fi/saapuva"
- warn_local_payload_url: "Näyttäisi siltä, että yrität asettaa webhookin paikalliseen URL-osoitteeseen. Paikalliseen osoitteeseen toimitetulla tapahtumalla voi olla sivuvaikutuksia tai se voi käyttäytyä odottamattomasti. Jatketaanko?"
secret_invalid: "Salausavaimessa ei voi olla tyhjiä merkkejä."
secret_too_short: "Salausavaimessa täytyy olla ainakin 12 merkkiä."
secret_placeholder: "Valinnainen merkkijono, käytetään luotaessa salausallekirjoitusta"
@@ -4477,7 +4473,6 @@ fi:
last_attempt: "Asennusprosessi ei päättynyt, viimeisin yritys:"
remote_branch: "Haaran nimi (valinnainen)"
public_key: "Anna tietovaraston käyttöoikeus seuraavalle julkiselle avaimelle:"
- public_key_note: "Kun olet syöttänyt kelvollisen yksityisen tietovaraston URL-osoitteen yllä, SSH-avain luodaan ja näytetään tässä."
install: "Asenna"
installed: "Asennettu"
install_popular: "Suosittuja"
@@ -5021,7 +5016,7 @@ fi:
suspended: "Hyllytetty?"
staged: "Esikäyttäjä?"
show_admin_profile: "Ylläpito"
- manage_user: "Hallinnoi käyttäjää"
+ manage_user: "Hallitse käyttäjää"
show_public_profile: "Näytä julkinen profiili"
impersonate: "Esiinny käyttäjänä"
action_logs: "Toimintaloki"
@@ -5156,7 +5151,6 @@ fi:
trust_level_2_users: "Käyttäjät luottamustasolla 2"
trust_level_3_requirements: "Luottamustason 3 vaatimukset"
trust_level_locked_tip: "luottamustaso on lukittu, järjestelmä ei ylennä tai alenna käyttäjää"
- trust_level_unlocked_tip: "luottamustason lukitus on poistettu, järjestelmä voi ylentää tai alentaa käyttäjän"
lock_trust_level: "Lukitse luottamustaso"
unlock_trust_level: "Poista luottamustason lukitus"
silenced_count: "Hiljennetty"
@@ -5216,23 +5210,18 @@ fi:
delete_confirm: "Oletko varma, että haluat poistaa tämän käyttäjäkentän?"
options: "Vaihtoehdot"
required:
- title: "Pakollinen rekisteröidyttäessä?"
enabled: "pakollinen"
disabled: "ei pakollinen"
editable:
- title: "Muokattavissa rekisteröitymisen jälkeen?"
enabled: "muokattavissa"
disabled: "ei muokattavissa"
show_on_profile:
- title: "Näytetään julkisessa profiilissa?"
enabled: "näytetään profiilissa"
disabled: "ei näytetä profiilissa"
show_on_user_card:
- title: "Näytetään käyttäjäkortilla?"
enabled: "näytetään käyttäjäkortilla"
disabled: "ei näytetä käyttäjäkortilla"
searchable:
- title: "Haettavissa?"
enabled: "haettavissa"
disabled: "ei haettavissa"
field_types:
@@ -5403,13 +5392,13 @@ fi:
upload_csv: Lataa CSV, jossa on joko käyttäjien sähköpostiosoitteita tai käyttäjätunnuksia
aborted: Lataa CSV, joka sisältää joko käyttäjien sähköpostiosoitteita tai käyttäjätunnuksia
success: CSV tuli perille ja %{count} käyttäjää saavat kunniamerkkinsä pian.
- csv_has_unmatched_users: "Seuraavat merkinnät ovat CSV-tiedostossa, mutta niitä ei voitu yhdistää olemassa oleviin käyttäjiin, joten he eivät saa merkkiä:"
- csv_has_unmatched_users_truncated_list: "CSV-tiedostossa oli %{count} merkintää, joita ei voitu yhdistää olemassa oleviin käyttäjiin, joten he eivät saa merkkiä. Koska yhdistämättömiä merkintöjä on paljon, vain 100 ensimmäistä näytetään:"
+ csv_has_unmatched_users: "Seuraavat merkinnät ovat CSV-tiedostossa, mutta niitä ei voitu yhdistää olemassa oleviin käyttäjiin, joten he eivät saa kunniamerkkiä:"
+ csv_has_unmatched_users_truncated_list: "CSV-tiedostossa oli %{count} merkintää, joita ei voitu yhdistää olemassa oleviin käyttäjiin, joten he eivät saa kunniamerkkiä. Koska yhdistämättömiä merkintöjä on paljon, vain 100 ensimmäistä näytetään:"
replace_owners: Poista kunniamerkki aiemmilta omistajilta
grant_existing_holders: Myönnä lisämerkkejä nykyisille kunniamerkkien haltijoille
emoji:
title: "Emoji"
- help: "Lisää uusi emoji, joka on kaikkien käytettävissä. Vedä ja pudota useita tiedostoja kerralla antamatta nimeä luodaksesi emojeja niiden tiedostonimillä. Valittua ryhmää käytetään kaikille tiedostoille, jotka lisätään samaan aikaan. Voit myös avata tiedostovalitsimen napsauttamalla Lisää uusi hymiö."
+ help: "Lisää uusi emoji, joka on kaikkien käytettävissä. Vedä ja pudota useita tiedostoja kerralla antamatta nimeä luodaksesi emojeja niiden tiedostonimillä. Valittua ryhmää käytetään kaikille tiedostoille, jotka lisätään samaan aikaan. Voit myös avata tiedostovalitsimen klikkaamalla Lisää uusi hymiö."
add: "Lisää uusi emoji"
choose_files: "Valitse tiedostot"
uploading: "Ladataan..."
diff --git a/config/locales/client.fr.yml b/config/locales/client.fr.yml
index fd5842eca2..cd3bd2c6e1 100644
--- a/config/locales/client.fr.yml
+++ b/config/locales/client.fr.yml
@@ -142,7 +142,7 @@ fr:
close: "fermer"
twitter: "Partager sur Twitter"
facebook: "Partager sur Facebook"
- email: "Envoyer par courriel"
+ email: "Envoyer par e-mail"
url: "Copier et partager l'URL"
action_codes:
public_topic: "a rendu ce sujet public %{when}"
@@ -176,17 +176,17 @@ fr:
banner:
enabled: "a mis à la une %{when}. Il sera affiché en haut de chaque page jusqu'à ce qu'il soit ignoré par un utilisateur."
disabled: "a supprimé de la une %{when}. Il ne sera plus affiché en haut de chaque page."
- forwarded: "a transmis le courriel ci-dessus"
+ forwarded: "a transmis l'e-mail ci-dessus"
topic_admin_menu: "actions du sujet"
skip_to_main_content: "Passer au contenu principal"
- emails_are_disabled: "Le courriel sortant a été désactivé par un administrateur. Aucune notification par courriel ne sera envoyée."
+ emails_are_disabled: "L'e-mail sortant a été désactivé par un administrateur. Aucune notification par e-mail ne sera envoyée."
emails_are_disabled_non_staff: "L'envoi d'e-mail est désactivé pour les utilisateurs ne faisant pas partie des responsables."
software_update_prompt:
message: "Nous avons fait une mise à jour du site, nous vous invitons donc à actualiser cette page pour éviter d'éventuels dysfonctionnements."
dismiss: "Ignorer"
bootstrap_mode_enabled:
- one: "Pour faciliter son lancement, votre nouveau site se trouve en mode d'amorçage. Chaque nouvel utilisateur se verra accorder le niveau de confiance 1 et recevra des résumés quotidiens par courriel. Ce mode sera automatiquement désactivé dès que votre site possédera %{count} utilisateur."
- other: "Pour faciliter son lancement, votre nouveau site se trouve en mode d'amorçage. Chaque nouvel utilisateur se verra accorder le niveau de confiance 1 et recevra des résumés quotidiens par courriel. Ce mode sera automatiquement désactivé dès que votre site possédera %{count} utilisateurs."
+ one: "Pour faciliter son lancement, votre nouveau site se trouve en mode d'amorçage. Chaque nouvel utilisateur se verra accorder le niveau de confiance 1 et recevra des résumés quotidiens par e-mail. Ce mode sera automatiquement désactivé dès que votre site possédera %{count} utilisateur."
+ other: "Pour faciliter son lancement, votre nouveau site se trouve en mode d'amorçage. Chaque nouvel utilisateur se verra accorder le niveau de confiance 1 et recevra des résumés quotidiens par e-mail. Ce mode sera automatiquement désactivé dès que votre site possédera %{count} utilisateurs."
bootstrap_mode_disabled: "Le mode d'amorçage sera désactivé dans les prochaines 24 heures."
bootstrap_invite_button_title: "Envoyer des invitations"
bootstrap_wizard_link_title: "Terminer l'assistant de configuration"
@@ -469,7 +469,7 @@ fr:
bio: "Biographie"
website: "Site Web"
username: "Nom d'utilisateur"
- email: "Courriel"
+ email: "E-mail"
name: "Nom"
fields: "Champs"
reject_reason: "Raison"
@@ -570,7 +570,7 @@ fr:
example_username: "nom d'utilisateur"
reject_reason:
title: "Pourquoi refusez-vous cet utilisateur ?"
- send_email: "Envoyer le courriel de refus"
+ send_email: "Envoyer l'e-mail de refus"
relative_time_picker:
minutes:
one: "minute"
@@ -674,7 +674,7 @@ fr:
title: "Ajouter des utilisateurs à %{group_name}"
description: "Saisissez une liste d'utilisateurs que vous souhaitez inviter dans le groupe ou collez-la dans une liste séparée par des virgules :"
usernames_placeholder: "noms d'utilisateur"
- usernames_or_emails_placeholder: "noms d'utilisateur ou courriels"
+ usernames_or_emails_placeholder: "noms d'utilisateur ou adresses e-mail"
notify_users: "Notifications aux utilisateurs"
set_owner: "Définir les utilisateurs comme propriétaires de ce groupe"
requests:
@@ -700,8 +700,8 @@ fr:
posting: Contribution
notification: Notification
email:
- title: "Courriel"
- status: "%{old_emails}/%{total_emails} courriels synchronisés via IMAP."
+ title: "E-mail"
+ status: "%{old_emails}/%{total_emails} e-mails synchronisés via IMAP."
enable_smtp: "Activer le SMTP"
enable_imap: "Activer l'IMAP"
test_settings: "Tester les paramètres"
@@ -711,10 +711,10 @@ fr:
settings_required: "Tous les paramètres sont requis. Veuillez renseigner chaque champ avant de valider."
smtp_settings_valid: "Paramètres SMTP valides."
smtp_title: "SMTP"
- smtp_instructions: "Quand le SMTP est activé au niveau du groupe, tous les courriels envoyés au nom de ce groupe seront expédiés en utilisant les paramètres SMTP indiqués ici plutôt qu'avec les paramètres SMTP globaux du site."
+ smtp_instructions: "Quand le SMTP est activé au niveau du groupe, tous les e-mails envoyés au nom de ce groupe seront expédiés en utilisant les paramètres SMTP indiqués ici plutôt qu'avec les paramètres SMTP globaux du site."
imap_title: "IMAP"
imap_additional_settings: "Paramètres additionnels"
- imap_instructions: 'Quand l''IMAP est activé au niveau du groupe, les courriels sont synchronisés entre la boîte de réception de ce groupe et la boîte de réception IMAP indiquée ici. Avant de pouvoir activer l''IMAP, le SMTP doit d''abord être activé, et des paramètres SMTP valides doivent être renseignés. L''identifiant et le mot de passe du serveur SMTP seront utilisés pour l''authentification auprès du serveur IMAP. Pour en savoir plus, consultez l''annonce de cette fonctionnalité sur Discourse Meta (en anglais).'
+ imap_instructions: 'Quand l''IMAP est activé au niveau du groupe, les e-mails sont synchronisés entre la boîte de réception de ce groupe et la boîte de réception IMAP indiquée ici. Avant de pouvoir activer l''IMAP, le SMTP doit d''abord être activé, et des paramètres SMTP valides doivent être renseignés. L''identifiant et le mot de passe du serveur SMTP seront utilisés pour l''authentification auprès du serveur IMAP. Pour en savoir plus, consultez l''annonce de cette fonctionnalité sur Discourse Meta (en anglais).'
imap_alpha_warning: "Attention : cette fonctionnalité est en test alpha. Seul Gmail est pris en charge officiellement. Son utilisation se fait à vos risques et périls !"
imap_settings_valid: "Paramètres IMAP valides."
smtp_disable_confirm: "En désactivant le SMTP, tous les paramètres SMTP et IMAP seront réinitialisés et ces fonctionnalités seront désactivées. Souhaitez-vous continuer ?"
@@ -736,12 +736,12 @@ fr:
settings:
title: "Paramètres"
allow_unknown_sender_topic_replies: "Autoriser les réponses par un expéditeur inconnu."
- allow_unknown_sender_topic_replies_hint: "Permet aux expéditeurs inconnus de répondre aux sujets du groupe. Si cette option n'est pas activée, les réponses provenant d'adresses courriel qui ne sont pas encore invitées dans le sujet créeront un nouveau sujet."
+ allow_unknown_sender_topic_replies_hint: "Permet aux expéditeurs inconnus de répondre aux sujets du groupe. Si cette option n'est pas activée, les réponses provenant d'adresses e-mail qui ne sont pas encore invitées dans le sujet créeront un nouveau sujet."
from_alias: "Alias de l'expéditeur"
from_alias_hint: "Alias à utiliser comme adresse d'expédition lors de l'envoi d'e-mails SMTP de groupe. Veuillez noter que cela peut ne pas être pris en charge par tous les prestataires de messagerie, nous vous invitons à consulter la documentation de votre prestataire."
mailboxes:
synchronized: "Boîte de réception à synchroniser"
- none_found: "Aucune boîte de réception n'a été trouvée pour ce compte de courriel."
+ none_found: "Aucune boîte de réception n'a été trouvée pour ce compte d'e-mail."
disabled: "Désactivée"
membership:
title: Adhésion
@@ -831,7 +831,7 @@ fr:
activity: "Activité"
members:
title: "Membres"
- filter_placeholder_admin: "nom d'utilisateur ou courriel"
+ filter_placeholder_admin: "nom d'utilisateur ou adresse e-mail"
filter_placeholder: "nom d'utilisateur"
remove_member: "Supprimer le membre"
remove_member_description: "Supprimer %{username} de ce groupe"
@@ -974,7 +974,7 @@ fr:
user_fields:
none: "(choisir une option)"
required: 'Veuillez saisir une valeur pour « %{name} »'
- same_as_password: 'Votre mot de passe ne doit pas être répété dans d''autres champs.'
+ same_as_password: "Votre mot de passe ne doit pas être répété dans d'autres champs."
user:
said: "%{username} :"
profile: "Profil"
@@ -1072,7 +1072,7 @@ fr:
no_bookmarks_search: "Aucun signet trouvé avec la requête de recherche fournie."
no_notifications_title: "Vous n'avez pas encore reçu de notifications"
no_notifications_body: >
- Des notifications s'afficheront ici pour vous informer de l'activité qui vous concerne directement sur le forum, telle que les réponses qui vous sont adressées, les personnes qui citent vos messages ou qui mentionnent votre pseudo, et les nouveaux messages publiés dans les sujets que vous suivez. Si vous ne vous êtes pas connecté(e) au forum depuis un moment, vous recevrez aussi ces notifications par courriel.
Le bouton %{icon} vous permettra de choisir les sujets, les catégories et les étiquettes pour lesquels vous souhaitez recevoir des notifications. Pour en savoir plus, consultez également vos préférences de notification.
+ Des notifications s'afficheront ici pour vous informer de l'activité qui vous concerne directement sur le forum, telle que les réponses qui vous sont adressées, les personnes qui citent vos messages ou qui mentionnent votre pseudo, et les nouveaux messages publiés dans les sujets que vous suivez. Si vous ne vous êtes pas connecté(e) au forum depuis un moment, vous recevrez aussi ces notifications par e-mail.
Le bouton %{icon} vous permettra de choisir les sujets, les catégories et les étiquettes pour lesquels vous souhaitez recevoir des notifications. Pour en savoir plus, consultez également vos préférences de notification.
no_other_notifications_title: "Vous n'avez pas encore reçu d'autres notifications"
no_other_notifications_body: >
Vous serez informé(e) dans ce panneau au sujet des autres types d'activité qui peuvent vous intéresser. Par exemple, lorsque quelqu'un crée un lien ou modifie un de vos messages.
@@ -1113,9 +1113,6 @@ fr:
tags_section: "Section Étiquettes"
tags_section_instruction: "Les étiquettes sélectionnées seront affichées dans la section Étiquettes de la barre latérale."
navigation_section: "Navigation"
- list_destination_instruction: "Lorsque je clique sur le lien d'une liste de sujets dans la barre latérale contenant des sujets nouveaux ou non lus, j'accède à"
- list_destination_default: "Par défaut"
- list_destination_unread_new: "Nouveau/Non lu"
change: "modifier"
featured_topic: "Sujet vedette"
moderator: "%{user} a le rôle de modérateur(rice)"
@@ -1133,12 +1130,12 @@ fr:
enabled: "Activer la liste de diffusion"
instructions: |
Ce paramètre remplace le résumé d'activité.
- Les sujets et catégories mis en sourdine ne sont pas inclus dans ces courriels.
- individual: "Envoyer un courriel pour chaque nouveau message"
- individual_no_echo: "Envoyer un courriel pour chaque nouveau message sauf les miens"
- many_per_day: "M'envoyer un courriel pour chaque nouveau message (environ %{dailyEmailEstimate} par jour)"
- few_per_day: "M'envoyer un courriel pour chaque nouveau message (environ 2 par jour)"
- warning: "Mode liste de diffusion activé. Les paramètres de notification par courriel sont remplacés."
+ Les sujets et catégories mis en sourdine ne sont pas inclus dans ces e-mails.
+ individual: "Envoyer un e-mail pour chaque nouveau message"
+ individual_no_echo: "Envoyer un e-mail pour chaque nouveau message sauf les miens"
+ many_per_day: "M'envoyer un e-mail pour chaque nouveau message (environ %{dailyEmailEstimate} par jour)"
+ few_per_day: "M'envoyer un e-mail pour chaque nouveau message (environ 2 par jour)"
+ warning: "Mode liste de diffusion activé. Les paramètres de notification par e-mail sont remplacés."
tag_settings: "Étiquettes"
watched_tags: "Surveillées"
watched_tags_instructions: "Vous surveillerez automatiquement tous les sujets marqués par ces étiquettes. Vous recevrez des notifications pour tous les nouveaux messages et sujets, et le nombre de nouveaux messages apparaîtra à côté du sujet."
@@ -1222,7 +1219,7 @@ fr:
account: "Compte"
security: "Sécurité"
profile: "Profil"
- emails: "Courriels"
+ emails: "E-mails"
notifications: "Notifications"
categories: "Catégories"
users: "Utilisateurs"
@@ -1231,11 +1228,11 @@ fr:
apps: "Applications"
sidebar: "Barre latérale"
change_password:
- success: "(courriel envoyé)"
- in_progress: "(courriel en cours d'envoi)"
+ success: "(e-mail envoyé)"
+ in_progress: "(e-mail en cours d'envoi)"
error: "(erreur)"
emoji: "émoji de cadenas"
- action: "Envoyer un courriel de réinitialisation du mot de passe"
+ action: "Envoyer un e-mail de réinitialisation du mot de passe"
set_password: "Définir le mot de passe"
choose_new: "Choisissez un nouveau mot de passe"
choose: "Choisissez un mot de passe"
@@ -1315,20 +1312,20 @@ fr:
taken: "Nous sommes désolés, ce nom d'utilisateur est déjà utilisé."
invalid: "Ce nom d'utilisateur est invalide. Il ne doit être composé que de lettres et de chiffres."
add_email:
- title: "Ajouter une adresse courriel"
+ title: "Ajouter une adresse e-mail"
add: "ajouter"
change_email:
- title: "Modifier l'adresse courriel"
- taken: "Nous sommes désolés, cette adresse courriel est indisponible."
- error: "Une erreur est survenue lors de la modification de l'adresse courriel. Cette adresse est peut-être déjà utilisée ?"
- success: "Nous avons envoyé un courriel à cette adresse. Merci de suivre les instructions."
- success_via_admin: "Nous avons envoyé un courriel à cette adresse. L'utilisateur devra suivre les instructions de confirmation qui y sont indiquées."
- success_staff: "Nous avons envoyé un courriel à votre adresse actuelle. Merci de suivre les instructions qui y figurent."
+ title: "Modifier l'adresse e-mail"
+ taken: "Nous sommes désolés, cette adresse e-mail est indisponible."
+ error: "Une erreur est survenue lors de la modification de l'adresse e-mail. Cette adresse est peut-être déjà utilisée ?"
+ success: "Nous avons envoyé un e-mail à cette adresse. Merci de suivre les instructions."
+ success_via_admin: "Nous avons envoyé un e-mail à cette adresse. L'utilisateur devra suivre les instructions de confirmation qui y sont indiquées."
+ success_staff: "Nous avons envoyé un e-mail à votre adresse actuelle. Merci de suivre les instructions qui y figurent."
change_avatar:
title: "Modifier votre image de profil"
gravatar: "%{gravatarName}, associé à"
gravatar_title: "Modifier votre avatar sur le site de %{gravatarName}"
- gravatar_failed: "Nous n'avons pas trouvé de %{gravatarName} associé à cette adresse courriel."
+ gravatar_failed: "Nous n'avons pas trouvé de %{gravatarName} associé à cette adresse e-mail."
refresh_gravatar_title: "Actualiser votre %{gravatarName}"
letter_based: "Image de profil attribuée par le système"
uploaded_avatar: "Avatar personnalisé"
@@ -1347,32 +1344,32 @@ fr:
title: "Sujet vedette"
instructions: "Un lien vers ce sujet sera ajouté à votre carte d'utilisateur et votre profil."
email:
- title: "Courriel"
- primary: "Adresse courriel principale"
- secondary: "Adresses courriel secondaires"
+ title: "E-mail"
+ primary: "Adresse e-mail principale"
+ secondary: "Adresses e-mail secondaires"
primary_label: "principale"
unconfirmed_label: "non confirmée"
- resend_label: "renvoyer le courriel de confirmation"
+ resend_label: "renvoyer l'e-mail de confirmation"
resending_label: "envoi en cours…"
- resent_label: "courriel envoyé"
- update_email: "Modifier l'adresse courriel"
- set_primary: "Définir comme adresse courriel principale"
- destroy: "Supprimer l'adresse courriel"
- add_email: "Ajouter une adresse courriel alternative"
- auth_override_instructions: "Le courriel peut être mis à jour à partir du fournisseur d'authentification."
- no_secondary: "Aucune adresse courriel secondaire"
+ resent_label: "e-mail envoyé"
+ update_email: "Modifier l'adresse e-mail"
+ set_primary: "Définir comme adresse e-mail principale"
+ destroy: "Supprimer l'adresse e-mail"
+ add_email: "Ajouter une adresse e-mail alternative"
+ auth_override_instructions: "L'e-mail peut être mis à jour à partir du fournisseur d'authentification."
+ no_secondary: "Aucune adresse e-mail secondaire"
instructions: "Ne sera pas visible publiquement."
- admin_note: "Remarque : un utilisateur administrateur modifiant l'adresse courriel d'un autre utilisateur non administrateur indique que l'utilisateur a perdu l'accès à son compte de messagerie d'origine. Un courriel de réinitialisation du mot de passe sera donc envoyé à sa nouvelle adresse. L'adresse courriel de l'utilisateur ne changera pas tant qu'il n'aura pas terminé le processus de réinitialisation du mot de passe."
- ok: "Nous vous enverrons un courriel de confirmation"
- required: "Veuillez saisir une adresse courriel"
- invalid: "Veuillez saisir une adresse courriel valide"
- authenticated: "Votre adresse courriel a été authentifiée par %{provider}"
+ admin_note: "Remarque : un utilisateur administrateur modifiant l'adresse e-mail d'un autre utilisateur non administrateur indique que l'utilisateur a perdu l'accès à son compte de messagerie d'origine. Un e-mail de réinitialisation du mot de passe sera donc envoyé à sa nouvelle adresse. L'adresse e-mail de l'utilisateur ne changera pas tant qu'il n'aura pas terminé le processus de réinitialisation du mot de passe."
+ ok: "Nous vous enverrons un e-mail de confirmation"
+ required: "Veuillez saisir une adresse e-mail"
+ invalid: "Veuillez saisir une adresse e-mail valide"
+ authenticated: "Votre adresse e-mail a été authentifiée par %{provider}"
invite_auth_email_invalid: "Votre e-mail d'invitation ne correspond pas à l'e-mail authentifié par %{provider}"
- authenticated_by_invite: "Votre adresse de courriel a été authentifiée par l'invitation"
- frequency_immediately: "Nous vous enverrons un courriel immédiatement si vous n'avez pas lu le contenu en question."
+ authenticated_by_invite: "Votre adresse e-mail a été authentifiée par l'invitation"
+ frequency_immediately: "Nous vous enverrons un e-mail immédiatement si vous n'avez pas lu le contenu en question."
frequency:
- one: "Nous vous enverrons des courriels seulement si nous ne vous avons pas vu(e) sur le site au cours de la dernière minute."
- other: "Nous vous enverrons des courriels seulement si nous ne vous avons pas vu(e) sur le site au cours des %{count} dernières minutes."
+ one: "Nous vous enverrons des e-mails seulement si nous ne vous avons pas vu(e) sur le site au cours de la dernière minute."
+ other: "Nous vous enverrons des e-mails seulement si nous ne vous avons pas vu(e) sur le site au cours des %{count} dernières minutes."
associated_accounts:
title: "Comptes associés"
connect: "Connecter"
@@ -1401,7 +1398,7 @@ fr:
too_short: "Votre nom d'utilisateur est trop court"
too_long: "Votre nom d'utilisateur est trop long"
checking: "Vérification de la disponibilité du nom d'utilisateur…"
- prefilled: "L'adresse courriel correspond à ce nom d'utilisateur enregistré"
+ prefilled: "L'adresse e-mail correspond à ce nom d'utilisateur enregistré"
required: "Veuillez entrer un nom d'utilisateur"
edit: "Modifier le nom d'utilisateur"
locale:
@@ -1435,7 +1432,7 @@ fr:
log_out: "Se déconnecter"
location: "Localisation"
website: "Site Web"
- email_settings: "Courriel"
+ email_settings: "E-mail"
hide_profile_and_presence: "Cacher mon profil public et mes statistiques"
enable_physical_keyboard: "Activer le support du clavier physique sur iPad"
text_size:
@@ -1456,12 +1453,12 @@ fr:
first_time: "La première fois qu'un message reçoit un « J'aime »"
never: "Jamais"
email_previous_replies:
- title: "Inclure les réponses précédentes en bas des courriels"
+ title: "Inclure les réponses précédentes en bas des e-mails"
unless_emailed: "sauf si déjà envoyées"
always: "toujours"
never: "jamais"
email_digests:
- title: "Lorsque je ne visite pas le site, m'envoyer un courriel avec un résumé des sujets et réponses populaires"
+ title: "Lorsque je ne visite pas le site, m'envoyer un e-mail avec un résumé des sujets et réponses populaires"
every_30_minutes: "toutes les 30 minutes"
every_hour: "toutes les heures"
daily: "tous les jours"
@@ -1474,8 +1471,8 @@ fr:
only_when_away: "seulement en cas d'absence"
never: "jamais"
email_messages_level: "Envoyez-moi un e-mail lorsque je reçois un message personnel"
- include_tl0_in_digests: "Inclure les contributions des nouveaux utilisateurs dans les résumés par courriel"
- email_in_reply_to: "En plus du message notifié par courriel, inclure un extrait du message auquel il répond"
+ include_tl0_in_digests: "Inclure les contributions des nouveaux utilisateurs dans les résumés par e-mail"
+ email_in_reply_to: "En plus du message notifié par e-mail, inclure un extrait du message auquel il répond"
other_settings: "Autre"
categories_settings: "Catégories"
new_topic_duration:
@@ -1515,7 +1512,7 @@ fr:
edit: "Modifier"
remove: "Supprimer"
copy_link: "Obtenir le lien"
- reinvite: "Renvoyer un courriel"
+ reinvite: "Renvoyer un e-mail"
reinvited: "Invitation renvoyée"
removed: "Supprimé"
search: "commencez votre saisie pour rechercher vos invitations…"
@@ -1542,8 +1539,8 @@ fr:
create: "Inviter"
generate_link: "Créer un lien d'invitation"
link_generated: "Voici votre lien d'invitation !"
- valid_for: "Le lien d'invitation est seulement valide pour cette adresse courriel : %{email}"
- single_user: "Inviter par courriel"
+ valid_for: "Le lien d'invitation est seulement valide pour cette adresse e-mail : %{email}"
+ single_user: "Inviter par e-mail"
multiple_user: "Inviter par lien"
invite_link:
title: "Lien d'invitation"
@@ -1567,7 +1564,7 @@ fr:
invite_to_topic: "Diriger vers ce sujet"
expires_at: "Ce lien expirera dans"
custom_message: "Message personnel (facultatif)"
- send_invite_email: "Enregistrer et envoyer le courriel"
+ send_invite_email: "Enregistrer et envoyer l'e-mail"
send_invite_email_instructions: "Restreindre l'invitation à l'adresse email pour envoyer un email d'invitation"
save_invite: "Enregistrer l'invitation"
invite_saved: "Invitation enregistrée."
@@ -1577,7 +1574,7 @@ fr:
instructions: |
jean@dupont.fr,nom_dun_groupe;nom_dun_autre_groupe,identifiant_numérique_du_sujet
-
יש לחפש אחר %{icon} כדי להחליט עבור אילו נושאים, קטגוריות ותגיות ברצונך לקבל התראות. למידע נוסף, ניתן לגשת אל העדפות ההתראות שלך.
no_other_notifications_title: "אין לך התראות נוספות עדיין"
+ no_other_notifications_body: >
+ בחלונית זו יופיע התראות על סוגים שונים של פעילויות שיכולות להתאים לך - למשל, כשמישהו מקשר או עורך או אחד הפוסטים שלך.
no_notifications_page_title: "אין לך התראות עדיין"
no_notifications_page_body: >
תישלחנה אליך התראות על פעילות שקשורה ישירות אליך, כולל תגובות לנושאים ולפוסטים שלך, כש@מאזכרים או מצטטים אותך או מגיבים לנושאים ברשימת המעקב שלך. כמו כן, התראות תישלחנה לכתובת הדוא״ל שלך כשלא נכנסת למערכת למשך זמן מה.
יש לחפש אחר %{icon} כדי להחליט עבור אילו נושאים, קטגוריות ותגיות ברצונך לקבל התראות. למידע נוסף, ניתן לגשת אל העדפות ההתראות שלך.
@@ -1225,9 +1228,9 @@ he:
tags_section: "סעיף תגיות"
tags_section_instruction: "התגיות הנבחרות תוצגנה תחת סעיף התגיות של סרגל הצד."
navigation_section: "ניווט"
- list_destination_instruction: "לחיצה על קישור רשימת נושאים עם נושאים חדשים או כאלו שלא נקראו בסרגל הצד, לקחת אותי אל"
- list_destination_default: "ברירת מחדל"
- list_destination_unread_new: "חדש/לא נקרא"
+ list_destination_instruction: "כשיש תוכן חדש בסרגל הצד…"
+ list_destination_default: "להשתמש בקישור ברירת המחדל ולהציג עיטור לפריטים חדשים"
+ list_destination_unread_new: "קישור לכאלו שלא נקראו/חדשים ולהציג את מניין הפריטים החדשים"
change: "שנה"
featured_topic: "נושא מומלץ"
moderator: "ל־%{user} יש תפקיד פיקוח"
@@ -3749,6 +3752,8 @@ he:
many: "לא-נקראו %{count} "
other: "לא-נקראו %{count} "
unseen:
+ title: "לא נראו"
+ lower_title: "לא נראו"
help: "נושאים חדשים ונושאים שאתם כרגע צופים או עוקבים אחריהם עם פוסטים שלא נקראו"
new:
lower_title_with_count:
@@ -4238,7 +4243,7 @@ he:
pending_count: "%{count} ממתינים"
welcome_topic_banner:
title: "כאן ניתן ליצור את נושא קבלת הפנים שלך"
- description: 'נושא קבלת הפנים שלך הוא הדבר הראשון שחברים חדשים יקראו. אפשר לחשוב על זה כמו על „נאום מעלית” או „הצהרת כוונות”. כדאי לתאר למי מיועדת הקהילה הזאת, מה אפשר לצפות למצוא כאן ומה מומלץ לעשות בהתחלה.'
+ description: "נושא קבלת הפנים שלך הוא הדבר הראשון שחברים חדשים יקראו. אפשר לחשוב על זה כמו על „נאום מעלית” או „הצהרת כוונות”. כדאי לתאר למי מיועדת הקהילה הזאת, מה אפשר לצפות למצוא כאן ומה מומלץ לעשות בהתחלה."
button_title: "להתחיל לערוך"
until: "עד:"
admin_js:
@@ -4519,7 +4524,6 @@ he:
go_back: "חזרה לרשימה"
payload_url: "URL של התוכן"
payload_url_placeholder: "https://example.com/postreceive"
- warn_local_payload_url: "נראה שניסית להגדיר את ההתליה (webhook) לכתובת מקומית. אירוע שנשלח לכתובת מקומית עשוי לגרום להשפעות חריגות או בלתי צפויות. להמשיך?"
secret_invalid: "אסור שהסוד יכיל תווי רווח כלשהם."
secret_too_short: "הסוד אמור להכיל לפחות 12 תווים."
secret_placeholder: "מחרוזת רשות, משמשת ליצירת חתימה"
@@ -4812,7 +4816,6 @@ he:
last_attempt: "תהליך ההתקנה לא הסתיים, ניסיון אחרון:"
remote_branch: "שם ענף (רשות)"
public_key: "להעניק למפתח הציבורי הבא גישה למאגר:"
- public_key_note: "לאחר מילוי כתובת אתר מאגר פרטית תקפה לעיל, מפתח SSH ייווצר ויוצג כאן."
install: "התקנה"
installed: "הותקן"
install_popular: "פופלארי"
@@ -5501,7 +5504,7 @@ he:
trust_level_2_users: "משתמשים בדרגת אמון 2"
trust_level_3_requirements: "דרישות דרגת אמון 3"
trust_level_locked_tip: "רמות האמון נעולה, המערכת לא תקדם או או תנמיך משתמשים"
- trust_level_unlocked_tip: "דרגת האמון אינה נעולה, המערכת תקדם ותנמיך דרגות של משתמשים"
+ trust_level_unlocked_tip: "דרגת האמון אינה נעולה, המערכת עלולה לקדם או להנמיך דרגות של משתמשים"
lock_trust_level: "נעילת דרגת אמון"
unlock_trust_level: "שחרור דרגת אמון מנעילה"
silenced_count: "מושתק"
@@ -5563,23 +5566,23 @@ he:
delete_confirm: "האם ברצונכם להסיר את שדה המשתמש הזה?"
options: "אפשרויות"
required:
- title: "נדרש בעת הרשמה?"
+ title: "נדרש בעת הרשמה"
enabled: "נדרש"
disabled: "לא נדרש"
editable:
- title: "ניתן לערוך לאחר הרשמה?"
+ title: "ניתן לערוך לאחר הרשמה"
enabled: "ניתן לערוך"
disabled: "לא ניתן לערוך"
show_on_profile:
- title: "להצגה בפרופיל הפומבי?"
+ title: "להצגה בפרופיל הפומבי"
enabled: "הצגה בפרופיל"
disabled: "לא מוצג בפרופיל"
show_on_user_card:
- title: "הצגה על כרטיס משתמש?"
+ title: "הצגה על כרטיס משתמש"
enabled: "מוצג על כרטיס משתמש"
disabled: "לא מוצג על כרטיס משתמש"
searchable:
- title: "זמין לחיפוש?"
+ title: "זמין לחיפוש"
enabled: "זמין לחיפוש"
disabled: "לא זמין לחיפוש"
field_types:
diff --git a/config/locales/client.hr.yml b/config/locales/client.hr.yml
index 6153c0a313..fc09369315 100644
--- a/config/locales/client.hr.yml
+++ b/config/locales/client.hr.yml
@@ -1151,7 +1151,6 @@ hr:
experimental_sidebar:
options: "Mogućnosti"
navigation_section: "Navigacija"
- list_destination_default: "Zadano"
change: "promijeni"
featured_topic: "Istaknuta tema"
moderator: "%{user} je moderator"
@@ -4216,7 +4215,6 @@ hr:
go_back: "Povratak na popis"
payload_url: "URL nosivosti"
payload_url_placeholder: "https://example.com/postreceive"
- warn_local_payload_url: "Čini se da pokušavate postaviti webhook na lokalni url. Događaj dostavljen na lokalnu adresu može uzrokovati nuspojave ili neočekivana ponašanja. Nastaviti?"
secret_invalid: "Tajna ne smije imati praznih znakova."
secret_too_short: "Tajna treba sadržavati najmanje 12 znakova."
secret_placeholder: "Izborni niz koji se koristi za generiranje potpisa"
@@ -4505,7 +4503,6 @@ hr:
is_private: "Tema je u privatnom git repozitoriju"
remote_branch: "Naziv podružnice (izborno)"
public_key: "Omogućite pristup sljedećem javnom ključu repo:"
- public_key_note: "Nakon unosa važećeg URL-a privatnog spremišta gore, ovdje će se generirati i prikazati SSH ključ."
install: "Instaliraj"
installed: "Instalirano"
install_popular: "Popularno"
@@ -5178,7 +5175,6 @@ hr:
trust_level_2_users: "Korisnici na razini povjerenja 2"
trust_level_3_requirements: "Predispozicije za razinu povjerenja 3"
trust_level_locked_tip: "razina povjerenja zaključana, sistem neće promovirati ili demotirati korisnika"
- trust_level_unlocked_tip: "razina povjerenja odključana, sistem će promovirati ili demotirati korisnika"
lock_trust_level: "Zaključaj razinu povjerenja"
unlock_trust_level: "Odključaj razinu povjerenja"
silenced_count: "Utišano"
@@ -5239,23 +5235,23 @@ hr:
delete_confirm: "Jeste li sigurni da želite obrisati to korisničko polje?"
options: "Mogućnosti"
required:
- title: "Potrebno pri registraciji?"
+ title: "Potrebno pri registraciji"
enabled: "potrebno"
disabled: "nije potrebno"
editable:
- title: "Izmijenjivo nakon registracije?"
+ title: "Izmijenjivo nakon registracije"
enabled: "izmijenjivo"
disabled: "nije izmijenjivo"
show_on_profile:
- title: "Prikaži na javnom profilu?"
+ title: "Prikaži na javnom profilu"
enabled: "prikazano na profilu"
disabled: "nije prikazano na profilu"
show_on_user_card:
- title: "Prikaži na korisničkoj kartici?"
+ title: "Prikaži na korisničkoj kartici"
enabled: "prikaži na korisničkoj kartici"
disabled: "nije prikazano na korisničkoj kartici"
searchable:
- title: "Može se pretraživati?"
+ title: "Može se pretraživati"
enabled: "može se pretraživati"
disabled: "nije moguće pretraživati"
field_types:
diff --git a/config/locales/client.hu.yml b/config/locales/client.hu.yml
index 46a05fefc8..30091a2456 100644
--- a/config/locales/client.hu.yml
+++ b/config/locales/client.hu.yml
@@ -1097,7 +1097,6 @@ hu:
experimental_sidebar:
options: "Beállítások"
navigation_section: "Navigáció"
- list_destination_default: "Alapértelmezett"
change: "módosítás"
featured_topic: "Kiemelt téma"
moderator: "%{user} egy moderátor"
@@ -4224,7 +4223,7 @@ hu:
enabled: "szerkeszthető"
disabled: "nem szerkeszthető"
show_on_profile:
- title: "Megjelenjen a nyilvános profilon?"
+ title: "Megjelenjen a nyilvános profilon"
enabled: "látható a profiljában"
disabled: "nem látható a profiljában"
field_types:
diff --git a/config/locales/client.hy.yml b/config/locales/client.hy.yml
index 2ed5cf8d24..b08417c590 100644
--- a/config/locales/client.hy.yml
+++ b/config/locales/client.hy.yml
@@ -840,7 +840,6 @@ hy:
experimental_sidebar:
options: "Տարբերակներ"
navigation_section: "Նավիգացիա"
- list_destination_default: "Լռելյայն"
change: "փոխել"
featured_topic: "Հանրահայտ թեմա"
moderator: "%{user}-ը մոդերատոր է"
@@ -3971,7 +3970,6 @@ hy:
trust_level_2_users: "Վստահության 2-րդ Մակարդակի Օգտատերեր"
trust_level_3_requirements: "Վստահության 3 Մակարդակի Պահանջներ"
trust_level_locked_tip: "վստահության մակարդակը արգելափակված է, համակարգը չի խթանի կամ խանգարի օգտատիրոջը"
- trust_level_unlocked_tip: "վստահության մակարդակը արգելաբացված է, համակարգը կարող է խթանել կամ խանգարել օգտատիրոջը"
lock_trust_level: "Արգելափակել Վստահության Մակարդակը"
unlock_trust_level: "Արգելաբացել Վստահության Մակարդակը"
silenced_count: "Լռեցված"
@@ -4026,19 +4024,15 @@ hy:
delete_confirm: "Դուք համոզվա՞ծ եք, որ ցանկանում եք ջնջել օգտատիրոջ այդ դաշտը:"
options: "Տարբերակներ"
required:
- title: "Պարտադի՞ր է գրանցվելիս:"
enabled: "պարտադիր է"
disabled: "պարտադիր չէ"
editable:
- title: "Խմբագրելի՞ է գրանցումից հետո:"
enabled: "խմբագրելի է"
disabled: "խմբագրելի չէ"
show_on_profile:
- title: "Ցու՞յց տալ հրապարակային պրոֆիլում"
enabled: "ցուցադրել պրոֆիլում"
disabled: "չցուցադրել պրոֆիլում"
show_on_user_card:
- title: "Ցուցադրե՞լ օգտատիրոջ քարտի վրա:"
enabled: "ցուցադրել օգտատիրոջ քարտի վրա"
disabled: "չցուցադրել օգտատիրոջ քարտի վրա"
field_types:
diff --git a/config/locales/client.id.yml b/config/locales/client.id.yml
index ef3d911995..c6aee8929e 100644
--- a/config/locales/client.id.yml
+++ b/config/locales/client.id.yml
@@ -936,7 +936,6 @@ id:
experimental_sidebar:
options: "Pilihan"
navigation_section: "Navigasi"
- list_destination_default: "Asal"
change: "ubah"
featured_topic: "Topik Unggulan"
moderator: "%{user} adalah moderator"
diff --git a/config/locales/client.it.yml b/config/locales/client.it.yml
index 706bc921ac..9f5a38e73d 100644
--- a/config/locales/client.it.yml
+++ b/config/locales/client.it.yml
@@ -970,7 +970,7 @@ it:
user_fields:
none: "(scegli un'opzione)"
required: 'Inserisci un valore per "%{name}"'
- same_as_password: 'La tua password non deve essere ripetuta in altri campi.'
+ same_as_password: "La tua password non deve essere ripetuta in altri campi."
user:
said: "%{username}:"
profile: "Profilo"
@@ -4153,7 +4153,6 @@ it:
go_back: "Torna all'elenco"
payload_url: "URL di Payload"
payload_url_placeholder: "https://example.com/postreceive"
- warn_local_payload_url: "Stai impostando un webhook che punta ad un indirizzo locale. Eventi inviati ad un indirizzo locale possono causare effetti collaterali o risultati inaspettati. Vuoi continuare?"
secret_invalid: "La chiave segreta non può contenere spazi vuoti."
secret_too_short: "La chiave segreta deve contenere almeno 12 caratteri."
secret_placeholder: "Una stringa facoltativa, usata per generare una firma"
@@ -4442,7 +4441,6 @@ it:
last_attempt: "Il processo di installazione non si è concluso, ultimo tentativo:"
remote_branch: "Nome del branch (opzionale)"
public_key: "Concedi l'accesso all'archivio con la seguente chiave pubblica:"
- public_key_note: "Dopo l’inserimento di un URL valido di archivio riservato, verrà generata e qui visualizzata una corrispondente chiave SSH."
install: "Installa"
installed: "Installato"
install_popular: "Popolare"
@@ -5120,7 +5118,6 @@ it:
trust_level_2_users: "Utenti con livello di attendibilità 2"
trust_level_3_requirements: "Requisiti per livello di attendibilità 3"
trust_level_locked_tip: "il livello di attendibilità è bloccato, il sistema non promuoverà né degraderà l'utente"
- trust_level_unlocked_tip: "il livello di attendibilità è sbloccato, il sistema può promuovere o degradare l'utente"
lock_trust_level: "Blocca livello di attendibilità"
unlock_trust_level: "Sblocca livello di attendibilità"
silenced_count: "Silenziati"
@@ -5180,23 +5177,18 @@ it:
delete_confirm: "Sicuro di voler cancellare il campo utente?"
options: "Opzioni"
required:
- title: "Obbligatorio durante l'iscrizione?"
enabled: "obbligatorio"
disabled: "non obbligatorio"
editable:
- title: "Modificabile dopo l'iscrizione?"
enabled: "modificabile"
disabled: "non modificabile"
show_on_profile:
- title: "Mostrare nel profilo pubblico?"
enabled: "mostrato nel profilo"
disabled: "non mostrato nel profilo"
show_on_user_card:
- title: "Mostrare sulla scheda utente?"
enabled: "mostrato sulla scheda utente"
disabled: "non mostrato sulla scheda utente"
searchable:
- title: "Ricercabile?"
enabled: "ricercabile"
disabled: "non ricercabile"
field_types:
diff --git a/config/locales/client.ja.yml b/config/locales/client.ja.yml
index 9af4023440..c4c7345ea9 100644
--- a/config/locales/client.ja.yml
+++ b/config/locales/client.ja.yml
@@ -289,7 +289,7 @@ ja:
confirm_delete: "このブックマークを削除してもよろしいですか?リマインダーも削除されます。"
confirm_clear: "このトピックのすべてのブックマークをクリアしてもよろしいですか?"
save: "保存"
- no_timezone: 'まだタイムゾーンを設定していないためリマインダーを設定することはできません。プロフィールで設定してください。'
+ no_timezone: 'まだタイムゾーンを設定していないためリマインダーを設定することはできません。プロファイルで設定してください。'
invalid_custom_datetime: "入力された日時が無効です。もう一度お試しください。"
list_permission_denied: "このユーザーのブックマークを表示する権限がありません。"
no_user_bookmarks: "ブックマークした投稿はありません。ブックマークを使用すると、特定の投稿を素早く参照できます。"
@@ -643,7 +643,7 @@ ja:
invite_members: "招待"
delete_member_confirm: "%{username} を %{group} グループから削除しますか?"
profile:
- title: プロフィール
+ title: プロファイル
interaction:
title: 交流
posting: 投稿
@@ -917,12 +917,12 @@ ja:
user_fields:
none: "(オプションを選択)"
required: '"%{name}" の値を入力してください'
- same_as_password: '他のフィールドでパスワードを繰り返し入力してはいけません。'
+ same_as_password: "他のフィールドでパスワードを繰り返し入力してはいけません。"
user:
said: "%{username}:"
- profile: "プロフィール"
+ profile: "プロファイル"
mute: "ミュート"
- edit: "プロフィールを編集"
+ edit: "プロファイルを編集"
download_archive:
button_text: "すべてダウンロード"
confirm: "投稿をダウンロードしてもよろしいですか?"
@@ -979,7 +979,7 @@ ja:
title: "クリア"
warning: "注目のトピックをクリアしてもよろしいですか?"
use_current_timezone: "現在のタイムゾーンを使用"
- profile_hidden: "このユーザーの公開プロフィールは非公開です。"
+ profile_hidden: "このユーザーの公開プロファイルは非公開です。"
expand_profile: "展開"
sr_expand_profile: "プロファイルの詳細を展開する"
collapse_profile: "折りたたむ"
@@ -1056,9 +1056,6 @@ ja:
tags_section: "タグセクション"
tags_section_instruction: "選択されたタグは、サイドバーのタグセクションに表示されます。"
navigation_section: "ナビゲーション"
- list_destination_instruction: "新規トピックまたは未読のトピックが含まれるサイドバーのトピックリストリンクをクリックすると、次に移動します:"
- list_destination_default: "デフォルト"
- list_destination_unread_new: "新規/未読"
change: "変更"
featured_topic: "注目のトピック"
moderator: "%{user} はモデレーターです"
@@ -1162,7 +1159,7 @@ ja:
preferences_nav:
account: "アカウント"
security: "セキュリティ"
- profile: "プロフィール"
+ profile: "プロファイル"
emails: "メール"
notifications: "通知"
categories: "カテゴリ"
@@ -1264,12 +1261,12 @@ ja:
success_via_admin: "このアドレスにメールを送信しました。ユーザーはメールに記載の確認手順に従う必要があります。"
success_staff: "現在のメールアドレスにメールを送信しました。確認手順に従ってください。"
change_avatar:
- title: "プロフィール画像を変更する"
+ title: "プロファイル画像を変更する"
gravatar: "%{gravatarName} 取得場所:"
gravatar_title: "%{gravatarName} のウェブサイトでアバターを変更する"
gravatar_failed: "このメールアドレスの %{gravatarName} は見つかりませんでした。"
refresh_gravatar_title: "%{gravatarName} を更新"
- letter_based: "システムプロフィール画像"
+ letter_based: "システムプロファイル画像"
uploaded_avatar: "カスタム画像"
uploaded_avatar_empty: "カスタム画像を追加する"
upload_title: "写真をアップロードする"
@@ -1277,14 +1274,14 @@ ja:
logo_small: "サイトの小さなロゴ。デフォルトで使用されます。"
use_custom: "またはカスタムアバターをアップロード:"
change_profile_background:
- title: "プロフィールヘッダー"
- instructions: "プロフィールヘッダーは中央揃えで、デフォルトの幅は 1110 px です。"
+ title: "プロファイルヘッダー"
+ instructions: "プロファイルヘッダーは中央揃えで、デフォルトの幅は 1110 px です。"
change_card_background:
title: "ユーザーカードの背景"
instructions: "背景画像は中央揃えで、デフォルトの幅は 590 px です。"
change_featured_topic:
title: "注目のトピック"
- instructions: "このトピックへのリンクは、あなたのユーザーカードとプロフィールに表示されます。"
+ instructions: "このトピックへのリンクは、あなたのユーザーカードとプロファイルに表示されます。"
email:
title: "メールアドレス"
primary: "プライマリーメールアドレス"
@@ -1374,7 +1371,7 @@ ja:
location: "場所"
website: "ウェブサイト"
email_settings: "メール"
- hide_profile_and_presence: "公開プロフィールとプレゼンス機能を非表示にする"
+ hide_profile_and_presence: "公開プロファイルとプレゼンス機能を非表示にする"
enable_physical_keyboard: "iPad で物理キーボードのサポートを有効にする"
text_size:
title: "テキストサイズ"
@@ -1512,7 +1509,7 @@ ja:
none: "このページに表示する招待はありません。"
text: "一括招待"
instructions: |
- john@smith.com,first_group_name;second_group_name,topic_id
HH:mm"
- long_date_with_year_with_linebreak: "D MMM 'YY
HH:mm"
+ long_date_without_year_with_linebreak: "D MMM
HH.mm"
+ long_date_with_year_with_linebreak: "D MMM 'YY
HH.mm"
wrap_ago: "%{date} geleden"
wrap_on: "op %{date}"
tiny:
- half_a_minute: "< 1m"
+ half_a_minute: "< 1 m"
less_than_x_seconds:
- one: "< %{count}s"
- other: "< %{count}s"
+ one: "< %{count} s"
+ other: "< %{count} s"
x_seconds:
- one: "%{count}s"
- other: "%{count}s"
+ one: "%{count} s"
+ other: "%{count} s"
less_than_x_minutes:
- one: "< %{count}m"
- other: "< %{count}m"
+ one: "< %{count} m"
+ other: "< %{count} m"
x_minutes:
- one: "%{count}m"
- other: "%{count}m"
+ one: "%{count} m"
+ other: "%{count} m"
about_x_hours:
- one: "%{count}u"
- other: "%{count}u"
+ one: "%{count} u"
+ other: "%{count} u"
x_days:
- one: "%{count}d"
- other: "%{count}d"
+ one: "%{count} d"
+ other: "%{count} d"
x_months:
- one: "%{count}mnd"
- other: "%{count}mnd"
+ one: "%{count} mnd"
+ other: "%{count} mnd"
about_x_years:
- one: "%{count}j"
- other: "%{count}j"
+ one: "%{count} j"
+ other: "%{count} j"
over_x_years:
- one: "> %{count}j"
- other: "> %{count}j"
+ one: "> %{count} j"
+ other: "> %{count} j"
almost_x_years:
- one: "%{count}j"
- other: "%{count}j"
+ one: "%{count} j"
+ other: "%{count} j"
date_month: "D MMM"
date_year: "MMM 'YY"
medium:
x_minutes:
- one: "%{count} min"
- other: "%{count} min"
+ one: "%{count} min."
+ other: "%{count} min."
x_hours:
one: "%{count} uur"
other: "%{count} uur"
@@ -89,8 +89,8 @@ nl:
date_year: "D MMM 'YY"
medium_with_ago:
x_minutes:
- one: "%{count} min geleden"
- other: "%{count} min geleden"
+ one: "%{count} min. geleden"
+ other: "%{count} min. geleden"
x_hours:
one: "%{count} uur geleden"
other: "%{count} uur geleden"
@@ -126,47 +126,47 @@ nl:
email: "Verzenden via e-mail"
url: "URL kopiëren en delen"
action_codes:
- public_topic: "heeft dit topic openbaar gemaakt op %{when}"
- private_topic: "heeft dit topic een privébericht gemaakt op %{when}"
- split_topic: "heeft dit topic gesplitst op %{when}"
- invited_user: "heeft %{who} uitgenodigd op %{when}"
- invited_group: "heeft %{who} uitgenodigd op %{when}"
- user_left: "%{who} heeft zichzelf uit dit bericht verwijderd op %{when}"
- removed_user: "heeft %{who} verwijderd op %{when}"
- removed_group: "heeft %{who} verwijderd op %{when}"
- autobumped: "automatisch gebumpt op %{when}"
+ public_topic: "heeft dit topic %{when} openbaar gemaakt"
+ private_topic: "heeft dit topic %{when} een privébericht gemaakt"
+ split_topic: "heeft dit topic %{when} gesplitst"
+ invited_user: "heeft %{who} %{when} uitgenodigd"
+ invited_group: "heeft %{who} %{when} uitgenodigd"
+ user_left: "%{who} heeft zichzelf %{when} uit dit bericht verwijderd"
+ removed_user: "heeft %{who} %{when} verwijderd"
+ removed_group: "heeft %{who} %{when} verwijderd"
+ autobumped: "heeft %{when} automatisch gebumpt"
autoclosed:
- enabled: "gesloten op %{when}"
- disabled: "geopend op %{when}"
+ enabled: "%{when} gesloten"
+ disabled: "%{when} geopend"
closed:
- enabled: "gesloten op %{when}"
- disabled: "geopend op %{when}"
+ enabled: "%{when} gesloten"
+ disabled: "%{when} geopend"
archived:
- enabled: "gearchiveerd op %{when}"
- disabled: "gedearchiveerd op %{when}"
+ enabled: "%{when} gearchiveerd"
+ disabled: "%{when} gedearchiveerd"
pinned:
- enabled: "vastgemaakt op %{when}"
- disabled: "losgemaakt op %{when}"
+ enabled: "%{when} vastgemaakt"
+ disabled: "%{when} losgemaakt"
pinned_globally:
- enabled: "globaal vastgemaakt op %{when}"
- disabled: "losgemaakt op %{when}"
+ enabled: "%{when} globaal vastgemaakt"
+ disabled: "%{when} losgemaakt"
visible:
- enabled: "zichtbaar gemaakt op %{when}"
- disabled: "onzichtbaar gemaakt op %{when}"
+ enabled: "%{when} zichtbaar gemaakt"
+ disabled: "%{when} onzichtbaar gemaakt"
banner:
- enabled: "heeft deze banner gemaakt op %{when}. De banner verschijnt bovenaan elke pagina, totdat de gebruiker deze verbergt."
- disabled: "heeft deze banner verwijderd op %{when}. De banner zal niet meer bovenaan elke pagina verschijnen."
+ enabled: "heeft deze banner %{when} gemaakt. De banner wordt weergegeven bovenaan elke pagina, totdat de gebruiker deze sluit."
+ disabled: "heeft deze banner %{when} verwijderd. De banner wordt niet meer weergegeven bovenaan elke pagina."
forwarded: "heeft de bovenstaande e-mail doorgestuurd"
topic_admin_menu: "topicacties"
- emails_are_disabled: "Alle uitgaande e-mail is uitgeschakeld door een beheerder. Er wordt geen enkele e-mailmelding verstuurd."
+ emails_are_disabled: "Alle uitgaande e-mail is uitgeschakeld door een beheerder. Er wordt geen enkele e-mailmelding gestuurd."
software_update_prompt:
- message: "We hebben deze site bijgewerkt, gelieve te vernieuwen, anders kan er onverwacht gedrag optreden."
+ message: "We hebben deze site bijgewerkt, dus vernieuw de pagina, anders kan er onverwacht gedrag optreden."
dismiss: "Negeren"
bootstrap_mode_enabled:
- one: "Om het opzetten van uw nieuwe website makkelijker te maken, bevindt u zich in bootstrapmodus. Aan alle nieuwe gebruikers wordt vertrouwensniveau 1 toegekend, en dagelijkse e-mailsamenvattingen zijn voor hen ingeschakeld. Dit wordt automatisch uitgeschakeld zodra %{count} gebruiker lid is geworden."
- other: "Om het opzetten van uw nieuwe website makkelijker te maken, bevindt u zich in bootstrapmodus. Aan alle nieuwe gebruikers wordt vertrouwensniveau 1 toegekend, en dagelijkse e-mailsamenvattingen zijn voor hen ingeschakeld. Dit wordt automatisch uitgeschakeld zodra %{count} gebruikers lid zijn geworden."
+ one: "Om het opzetten van je nieuwe website makkelijker te maken, bevind je je in de bootstrapmodus. Aan alle nieuwe gebruikers wordt vertrouwensniveau 1 toegekend en dagelijkse e-mailsamenvattingen zijn ingeschakeld voor hen. Dit wordt automatisch uitgeschakeld zodra %{count} gebruiker lid is geworden."
+ other: "Om het opzetten van je nieuwe website makkelijker te maken, bevind je je in de bootstrapmodus. Aan alle nieuwe gebruikers wordt vertrouwensniveau 1 toegekend en dagelijkse e-mailsamenvattingen zijn ingeschakeld voor hen. Dit wordt automatisch uitgeschakeld zodra %{count} gebruikers lid zijn geworden."
bootstrap_mode_disabled: "De bootstrapmodus wordt binnen 24 uur uitgeschakeld."
- bootstrap_invite_button_title: "Uitnodigingen versturen"
+ bootstrap_invite_button_title: "Uitnodigingen verzenden"
themes:
default_description: "Standaard"
s3:
@@ -174,11 +174,11 @@ nl:
ap_northeast_1: "Azië Pacifisch (Tokio)"
ap_northeast_2: "Azië Pacifisch (Seoel)"
ap_east_1: "Azië Pacifisch (Hong Kong)"
- ap_south_1: "Azië Pacifisch (Bombay)"
+ ap_south_1: "Azië Pacifisch (Mumbai)"
ap_southeast_1: "Azië Pacifisch (Singapore)"
ap_southeast_2: "Azië Pacifisch (Sydney)"
ca_central_1: "Canada (Centraal)"
- cn_north_1: "China (Peking)"
+ cn_north_1: "China (Beijing)"
cn_northwest_1: "China (Ningxia)"
eu_central_1: "EU (Frankfurt)"
eu_north_1: "EU (Stockholm)"
@@ -201,26 +201,26 @@ nl:
yes_value: "Ja"
ok_value: "OK"
cancel_value: "Annuleren"
- submit: "Versturen"
+ submit: "Verzenden"
delete: "Verwijderen"
- generic_error: "Sorry, er is iets fout gegaan."
- generic_error_with_reason: "Er is iets fout gegaan: %{error}"
+ generic_error: "Sorry, er is een fout opgetreden."
+ generic_error_with_reason: "Er is een fout opgetreden: %{error}"
sign_up: "Registreren"
log_in: "Aanmelden"
age: "Leeftijd"
joined: "Lid sinds"
admin_title: "Beheer"
- show_more: "meer tonen"
+ show_more: "meer weergeven"
show_help: "opties"
- links: "Koppelingen"
+ links: "Links"
links_lowercase:
- one: "koppeling"
- other: "koppelingen"
+ one: "link"
+ other: "links"
faq: "FAQ"
guidelines: "Richtlijnen"
privacy_policy: "Privacybeleid"
privacy: "Privacy"
- tos: "Algemene Voorwaarden"
+ tos: "Gebruiksvoorwaarden"
rules: "Regels"
conduct: "Gedragscode"
mobile_view: "Mobiele weergave"
@@ -230,14 +230,14 @@ nl:
read_more: "meer lezen"
more: "Meer"
x_more:
- one: "%{count} Meer"
- other: "%{count} Meer"
+ one: "Nog %{count}"
+ other: "Nog %{count}"
never: "nooit"
every_30_minutes: "elke 30 minuten"
every_hour: "elk uur"
daily: "dagelijks"
weekly: "wekelijks"
- every_month: "elke maand"
+ every_month: "maandelijks"
every_six_months: "elke zes maanden"
max_of_count: "max. %{count}"
character_count:
@@ -247,7 +247,7 @@ nl:
aria_label: "Filteren op periode"
related_messages:
title: "Gerelateerde berichten"
- see_all: 'Alle berichten van @%{username}bekijken...'
+ see_all: 'Alle berichten van @%{username} weergeven...'
suggested_topics:
title: "Aanbevolen topics"
pm_title: "Voorgestelde berichten"
@@ -271,36 +271,36 @@ nl:
contact: "Contact"
contact_info: "Neem in het geval van een kritieke kwestie of dringende vraagstukken in verband met deze website contact met ons op via %{contact_info}."
bookmarked:
- title: "Bladwijzer maken"
+ title: "Bladwijzer"
edit_bookmark: "Bladwijzer bewerken"
clear_bookmarks: "Bladwijzers wissen"
help:
bookmark: "Klik om een bladwijzer voor het eerste bericht van dit topic te maken"
unbookmark: "Klik om alle bladwijzers in dit topic te verwijderen"
bookmarks:
- created: "U hebt een bladwijzer voor dit bericht gemaakt. %{name}"
+ created: "Je hebt een bladwijzer voor dit bericht gemaakt. %{name}"
create: "Bladwijzer maken"
edit: "Bladwijzer bewerken"
- not_bookmarked: "bladwijzer voor dit bericht maken"
- created_with_reminder: "U hebt een bladwijzer voor dit bericht gemaakt met een herinnering voor %{date}. %{name}"
+ not_bookmarked: "bladwijzer maken voor dit bericht"
+ created_with_reminder: "Je hebt een bladwijzer voor dit bericht gemaakt met een herinnering voor %{date}. %{name}"
remove: "Bladwijzer verwijderen"
delete: "Bladwijzer verwijderen"
- confirm_delete: "Weet u zeker dat u deze bladwijzer wilt verwijderen? De herinnering wordt ook verwijderd."
- confirm_clear: "Weet u zeker dat u alle bladwijzers van dit topic wilt verwijderen?"
+ confirm_delete: "Weet je zeker dat je deze bladwijzer wilt verwijderen? De herinnering wordt ook verwijderd."
+ confirm_clear: "Weet je zeker dat je alle bladwijzers van dit topic wilt verwijderen?"
save: "Opslaan"
- no_timezone: 'U hebt nog geen tijdzone ingesteld. Hierdoor kunt u geen herinneringen instellen. Stel er een in in uw profiel.'
- invalid_custom_datetime: "De datum en tijd die u hebt opgegeven is ongeldig, probeer het opnieuw."
- list_permission_denied: "U hebt geen toestemming om de bladwijzers van deze gebruiker te bekijken."
- no_user_bookmarks: "U hebt geen bladwijzers voor berichten; via bladwijzers kunt u snel bepaalde berichten raadplegen."
+ no_timezone: 'Je hebt nog geen tijdzone ingesteld. Hierdoor kun je geen herinneringen instellen. Stel er een in in je profiel.'
+ invalid_custom_datetime: "De datum en tijd die je hebt opgegeven zijn ongeldig, probeer het opnieuw."
+ list_permission_denied: "Je hebt geen toestemming om de bladwijzers van deze gebruiker te bekijken."
+ no_user_bookmarks: "Je hebt geen bladwijzers voor berichten. Met bladwijzers kun je snel bepaalde berichten raadplegen."
auto_delete_preference:
when_reminder_sent: "Bladwijzer verwijderen"
- search_placeholder: "Bladwijzers doorzoeken op naam, topictitel of berichtinhoud"
+ search_placeholder: "Zoek bladwijzers op naam, topictitel of berichtinhoud"
search: "Zoeken"
reminders:
today_with_time: "vandaag om %{time}"
tomorrow_with_time: "morgen om %{time}"
- at_time: "op %{date_time}"
- existing_reminder: "U hebt een herinnering voor deze bladwijzer ingesteld die %{at_date_time} wordt verzonden"
+ at_time: "om %{date_time}"
+ existing_reminder: "Je hebt een herinnering voor deze bladwijzer ingesteld die %{at_date_time} wordt verzonden"
copy_codeblock:
copied: "gekopieerd!"
drafts:
@@ -308,29 +308,29 @@ nl:
label_with_count: "Concepten (%{count})"
resume: "Hervatten"
remove: "Verwijderen"
- remove_confirmation: "Weet u zeker dat u dit concept wilt verwijderen?"
- new_topic: "Nieuw-topicconcept"
- new_private_message: "Nieuw privé-bericht concept"
+ remove_confirmation: "Weet je zeker dat je dit concept wilt verwijderen?"
+ new_topic: "Nieuw topicconcept"
+ new_private_message: "Nieuw privéberichtconcept"
topic_reply: "Conceptantwoord"
abandon:
- confirm: "U hebt een concept in uitvoering voor dit topic. Wat wilt u er mee doen?"
- yes_value: "Negeren"
+ confirm: "Je hebt een concept in uitvoering voor dit topic. Wat wil je ermee doen?"
+ yes_value: "Weggooien"
no_value: "Bewerken hervatten"
topic_count_categories:
- one: "%{count} nieuw of bijgewerkt topic bekijken"
- other: "%{count} nieuwe of bijgewerkte topics bekijken"
+ one: "%{count} nieuw of bijgewerkt topic weergeven"
+ other: "%{count} nieuwe of bijgewerkte topics weergeven"
topic_count_latest:
- one: "%{count} nieuw of bijgewerkt topic bekijken"
- other: "%{count} nieuwe of bijgewerkte topics bekijken"
+ one: "%{count} nieuw of bijgewerkt topic weergeven"
+ other: "%{count} nieuwe of bijgewerkte topics weergeven"
topic_count_unseen:
- one: "%{count} nieuw of bijgewerkt topic bekijken"
- other: "%{count} nieuwe of bijgewerkte topics bekijken"
+ one: "%{count} nieuw of bijgewerkt topic weergeven"
+ other: "%{count} nieuwe of bijgewerkte topics weergeven"
topic_count_unread:
- one: "%{count} ongelezen topic bekijken"
- other: "%{count} ongelezen topics bekijken"
+ one: "%{count} ongelezen topic weergeven"
+ other: "%{count} ongelezen topics weergeven"
topic_count_new:
- one: "%{count} nieuw topic bekijken"
- other: "%{count} nieuwe topics bekijken"
+ one: "%{count} nieuw topic weergeven"
+ other: "%{count} nieuwe topics weergeven"
preview: "voorbeeld"
cancel: "annuleren"
deleting: "Verwijderen..."
@@ -348,15 +348,15 @@ nl:
disable: "Uitschakelen"
continue: "Doorgaan"
undo: "Ongedaan maken"
- revert: "Terugzetten"
+ revert: "Herstellen"
failed: "Mislukt"
switch_to_anon: "Anonieme modus starten"
switch_from_anon: "Anonieme modus verlaten"
banner:
- close: "Deze banner negeren"
+ close: "Deze banner sluiten"
edit: "Bewerken"
pwa:
- install_banner: "Wilt u %{title} op dit apparaat installeren?"
+ install_banner: "Wil je %{title} installeren op dit apparaat?"
choose_topic:
none_found: "Geen topics gevonden."
title:
@@ -370,7 +370,7 @@ nl:
review:
order_by: "Sorteren op"
date_filter: "Geplaatst tussen"
- in_reply_to: "in reactie op"
+ in_reply_to: "in antwoord op"
explain:
why: "leg uit waarom dit item in de wachtrij is beland"
title: "Beoordeelbare scores"
@@ -381,7 +381,7 @@ nl:
score_to_hide: "Score voor verbergen van bericht"
take_action_bonus:
name: "heeft actie ondernomen"
- title: "Wanneer een staflid kiest voor het ondernemen van actie, wordt een bonus aan de markering gegeven."
+ title: "Wanneer een staflid ervoor kiest actie te ondernemen, ontvangt de markering een bonus."
user_accuracy_bonus:
name: "gebruikersnauwkeurigheid"
title: "Gebruikers waarvan markeringen in het verleden zijn geaccordeerd ontvangen een bonus."
@@ -389,15 +389,15 @@ nl:
name: "vertrouwensniveau"
title: "Beoordeelbare items die door gebruikers met een hoger vertrouwensniveau zijn gemaakt hebben een hogere score."
type_bonus:
- name: "type bonus"
+ name: "typebonus"
title: "Aan bepaalde beoordeelbare typen kan door stafleden een bonus worden toegekend om ze hogere prioriteit te geven."
claim_help:
- optional: "U kunt dit item opeisen om te voorkomen dat anderen het beoordelen."
- required: "U moet items opeisen voordat u ze kunt beoordelen."
- claimed_by_you: "U hebt dit item opgeëist en kunt het beoordelen."
+ optional: "Je kunt dit item claimen om te voorkomen dat anderen het beoordelen."
+ required: "Je moet items claimen voordat je ze kunt beoordelen."
+ claimed_by_you: "Je hebt dit item geclaimd en kunt het beoordelen."
claimed_by_other: "Dit item kan alleen worden beoordeeld door %{username}."
claim:
- title: "dit topic opeisen"
+ title: "dit topic claimen"
unclaim:
help: "deze claim verwijderen"
awaiting_approval: "Wacht op goedkeuring"
@@ -407,25 +407,25 @@ nl:
save_changes: "Wijzigingen opslaan"
title: "Instellingen"
priorities:
- title: "Beoordeelbare prioriteiten"
+ title: "Prioriteiten voor beoordeelbare items"
moderation_history: "Moderatiegeschiedenis"
- view_all: "Alle bekijken"
+ view_all: "Alles weergeven"
grouped_by_topic: "Gegroepeerd op topic"
none: "Er zijn geen items om te beoordelen."
- view_pending: "wachtende bekijken"
+ view_pending: "wachtende weergeven"
topic_has_pending:
one: "Dit topic heeft %{count} bericht dat op goedkeuring wacht"
other: "Dit topic heeft %{count} berichten die op goedkeuring wachten"
title: "Beoordelen"
topic: "Topic:"
- filtered_topic: "U hebt op beoordeelbare inhoud in één topic gefilterd."
+ filtered_topic: "Je hebt op beoordeelbare inhoud in één topic gefilterd."
filtered_user: "Gebruiker"
filtered_reviewed_by: "Beoordeeld door"
- show_all_topics: "alle topics tonen"
+ show_all_topics: "alle topics weergeven"
deleted_post: "(bericht verwijderd)"
deleted_user: "(gebruiker verwijderd)"
user:
- bio: "Biografie"
+ bio: "Bio"
website: "Website"
username: "Gebruikersnaam"
email: "E-mailadres"
@@ -461,7 +461,7 @@ nl:
edit: "Bewerken"
save: "Opslaan"
cancel: "Annuleren"
- new_topic: "Goedkeuren van dit item maakt een nieuw topic"
+ new_topic: "Door dit item goed te keuren, wordt eeen nieuw topic gemaakt"
filters:
all_categories: "(alle categorieën)"
type:
@@ -474,18 +474,18 @@ nl:
orders:
score: "Score"
score_asc: "Score (omgekeerd)"
- created_at: "Lid sinds"
+ created_at: "Gemaakt op"
created_at_asc: "Gemaakt op (omgekeerd)"
priority:
title: "Minimale prioriteit"
any: "(alle)"
low: "Laag"
- medium: "Gemiddeld"
+ medium: "Middel"
high: "Hoog"
conversation:
- view_full: "volledige conversatie bekijken"
+ view_full: "volledige conversatie weergeven"
scores:
- about: "Deze score wordt berekend op basis van het vertrouwen van de melder, de nauwkeurigheid van zijn of haar eerdere markeringen, en de prioriteit van het item dat wordt gemeld."
+ about: "Deze score wordt berekend op basis van het vertrouwen van de melder, de nauwkeurigheid van diens eerdere markeringen en de prioriteit van het item dat wordt gemeld."
score: "Score"
date: "Datum"
type: "Type"
@@ -494,7 +494,7 @@ nl:
reviewed_by: "Beoordeeld door"
statuses:
pending:
- title: "In wachtrij"
+ title: "Wachtend"
approved:
title: "Goedgekeurd"
rejected:
@@ -520,16 +520,16 @@ nl:
reviewable_post:
title: "Bericht"
approval:
- title: "Bericht heeft goedkeuring nodig"
- description: "We hebben uw nieuwe bericht ontvangen, maar dit moet eerst door een moderator worden goedgekeurd voordat het zichtbaar wordt. Heb geduld."
+ title: "Bericht vereist goedkeuring"
+ description: "We hebben je nieuwe bericht ontvangen, maar dit moet eerst door een moderator worden goedgekeurd voordat het zichtbaar wordt. Heb geduld."
pending_posts:
- one: "U hebt %{count} wachtend bericht."
- other: "U hebt %{count} wachtende berichten."
+ one: "Je hebt %{count} wachtend bericht."
+ other: "Je hebt %{count} wachtende berichten."
ok: "OK"
example_username: "gebruikersnaam"
reject_reason:
- title: "Waarom keurt u deze gebruiker af?"
- send_email: "Afwijzingsmail verzenden"
+ title: "Waarom wijs je deze gebruiker af?"
+ send_email: "Afwijzings-e-mail verzenden"
relative_time_picker:
minutes:
one: "minuut"
@@ -575,10 +575,10 @@ nl:
user_action:
user_posted_topic: "%{user} heeft het topic geplaatst"
you_posted_topic: "U hebt het topic geplaatst"
- user_replied_to_post: "%{user} heeft op %{post_number} geantwoord"
- you_replied_to_post: "U hebt op %{post_number} geantwoord"
- user_replied_to_topic: "%{user} heeft op het topic geantwoord"
- you_replied_to_topic: "U hebt op het topic geantwoord"
+ user_replied_to_post: "%{user} heeft geantwoord op %{post_number}"
+ you_replied_to_post: "U hebt geantwoord op %{post_number}"
+ user_replied_to_topic: "%{user} heeft geantwoord op het topic"
+ you_replied_to_topic: "U hebt geantwoord op het topic"
user_mentioned_user: "%{user} heeft %{another_user} genoemd"
user_mentioned_you: "%{user} heeft u genoemd"
you_mentioned_user: "U hebt %{another_user} genoemd"
@@ -610,7 +610,7 @@ nl:
other: "%{count} gebruikers"
edit_columns:
save: "Opslaan"
- reset_to_default: "Standaardwaarden terugzetten"
+ reset_to_default: "Standaardwaarde herstellen"
group:
all: "alle groepen"
group_histories:
@@ -622,30 +622,30 @@ nl:
remove_user_as_group_owner: "Eigenaar intrekken"
groups:
member_added: "Toegevoegd"
- member_requested: "Aangevraagd:"
+ member_requested: "Verzocht op"
add_members:
- title: "Voeg gebruikers toe aan %{group_name}"
- description: "Voer een lijst in met gebruikers die u voor de groep wilt uitnodigen of plak hier een door komma's gescheiden lijst:"
+ title: "Gebruikers toevoegen aan %{group_name}"
+ description: "Voer een lijst in met gebruikers die je wilt uitnodigen voor de groep of plak hier een door komma's gescheiden lijst:"
usernames_placeholder: "gebruikersnamen"
usernames_or_emails_placeholder: "gebruikersnamen of e-mailadressen"
notify_users: "Gebruikers een melding sturen"
set_owner: "Stel gebruikers in als eigenaar van deze groep"
requests:
- title: "Aanvragen"
+ title: "Verzoeken"
reason: "Reden"
accept: "Accepteren"
accepted: "geaccepteerd"
deny: "Weigeren"
denied: "geweigerd"
- undone: "aanvraag ongedaan gemaakt"
- handle: "lidmaatschapsaanvraag behandelen"
+ undone: "verzoek ongedaan gemaakt"
+ handle: "lidmaatschapsverzoek behandelen"
manage:
title: "Beheren"
name: "Naam"
full_name: "Volledige naam"
add_members: "Gebruikers toevoegen"
invite_members: "Uitnodigen"
- delete_member_confirm: "'%{username}' uit de groep '%{group}' verwijderen?"
+ delete_member_confirm: "'%{username}' verwijderen uit de groep '%{group}'?"
profile:
title: Profiel
interaction:
@@ -654,21 +654,21 @@ nl:
notification: Melding
email:
title: "E-mailadres"
- status: "%{old_emails} / %{total_emails} e-mails via IMAP gesynchroniseerd."
+ status: "%{old_emails} / %{total_emails} e-mails gesynchroniseerd via IMAP."
enable_smtp: "SMTP inschakelen"
enable_imap: "IMAP inschakelen"
- test_settings: "Test Instellingen"
+ test_settings: "Instellingen testen"
save_settings: "Instellingen opslaan"
last_updated: "Laatst bijgewerkt:"
last_updated_by: "door"
- smtp_settings_valid: "SMTP instellingen geldig."
+ smtp_settings_valid: "SMTP-instellingen geldig."
smtp_title: "SMTP"
imap_title: "IMAP"
- imap_additional_settings: "Extra instellingen"
+ imap_additional_settings: "Aanvullende instellingen"
prefill:
gmail: "GMail"
credentials:
- title: "Referenties"
+ title: "Aanmeldgegevens"
smtp_server: "SMTP-server"
smtp_port: "SMTP-poort"
smtp_ssl: "SSL gebruiken voor SMTP"
@@ -679,10 +679,10 @@ nl:
password: "Wachtwoord"
settings:
title: "Instellingen"
- allow_unknown_sender_topic_replies: "Sta topic antwoorden van onbekende afzenders toe."
+ allow_unknown_sender_topic_replies: "Sta topicantwoorden van onbekende afzenders toe."
mailboxes:
- synchronized: "Gesynchroniseerd postvak"
- none_found: "Geen postvakken gevonden in deze e-mailaccount."
+ synchronized: "Gesynchroniseerde mailbox"
+ none_found: "Geen mailboxen gevonden op dit e-mailaccount."
disabled: "Uitgeschakeld"
membership:
title: Lidmaatschap
@@ -690,23 +690,23 @@ nl:
categories:
title: Categorieën
long_title: "Standaardmeldingen voor categorieën"
- description: "Wanneer gebruikers aan deze groep worden toegevoegd, worden hun instellingen voor categoriemeldingen op deze standaardwaarden ingesteld. Daarna kunnen ze deze wijzigen."
- watched_categories_instructions: "Automatisch alle topics in deze categorieën in de gaten houden. Groepsleden ontvangen meldingen bij alle nieuwe berichten en topics, en het aantal nieuwe berichten verschijnt ook naast het topic."
- tracked_categories_instructions: "Automatisch alle topics in deze categorieën volgen. Het aantal nieuwe berichten verschijnt naast het topic."
+ description: "Wanneer gebruikers aan deze groep worden toegevoegd, worden hun instellingen voor categoriemeldingen ingesteld op deze standaardwaarden. Daarna kunnen ze deze wijzigen."
+ watched_categories_instructions: "Observeer automatisch alle topics in deze categorieën. Groepsleden ontvangen meldingen bij alle nieuwe berichten en topics en het aantal nieuwe berichten wordt weergegeven naast het topic."
+ tracked_categories_instructions: "Volg automatisch alle topics in deze categorieën. Het aantal nieuwe berichten wordt weergegeven naast het topic."
watching_first_post_categories_instructions: "Gebruikers ontvangen een melding bij het eerste bericht in elk nieuw topic in deze categorieën."
regular_categories_instructions: "Als deze categorieën zijn gedempt, wordt het dempen opgeheven voor groepsleden. Gebruikers ontvangen een melding als ze worden genoemd of als iemand erop antwoordt."
- muted_categories_instructions: "Gebruikers ontvangen geen enkele melding over nieuwe topics in deze categorieën, en ze verschijnen niet op de pagina's Categorieën of Nieuwste."
+ muted_categories_instructions: "Gebruikers ontvangen geen enkele melding over nieuwe topics en berichten in deze categorieën en deze worden niet weergegeven op de pagina's Categorieën of Nieuwste."
tags:
title: Tags
long_title: "Standaardmeldingen voor tags"
- description: "Wanneer gebruikers aan deze groep worden toegevoegd, worden hun instellingen voor tagmeldingen op deze standaardwaarden ingesteld. Daarna kunnen ze deze wijzigen."
- watched_tags_instructions: "Automatisch alle topics met deze tags in de gaten houden. Groepsleden ontvangen meldingen bij alle nieuwe berichten en topics, en het aantal nieuwe berichten verschijnt ook naast het topic."
- tracked_tags_instructions: "Automatisch alle topics met deze tags volgen. Het aantal nieuwe berichten verschijnt naast het topic."
+ description: "Wanneer gebruikers aan deze groep worden toegevoegd, worden hun instellingen voor tagmeldingen ingesteld op deze standaardwaarden. Daarna kunnen ze deze wijzigen."
+ watched_tags_instructions: "Observeer automatisch alle topics met deze tags. Groepsleden ontvangen meldingen bij alle nieuwe berichten en topics en het aantal nieuwe berichten wordt weergegeven naast het topic."
+ tracked_tags_instructions: "Volg automatisch alle topics met deze tags. Het aantal nieuwe berichten wordt weergegeven naast het topic."
watching_first_post_tags_instructions: "Gebruikers ontvangen een melding bij het eerste bericht in elk nieuw topic met deze tags."
- regular_tags_instructions: "Als deze tags zijn gedempt, wordt het dempen opgeheven voor groepsleden. Gebruikers ontvangen een melding als ze worden genoemd of als iemand erop antwoordt."
- muted_tags_instructions: "Gebruikers ontvangen geen enkele melding over nieuwe topics met deze tags, en ze verschijnen niet in Nieuwste."
+ regular_tags_instructions: "Als deze tags zijn gedempt, wordt het dempen opgeheven voor groepsleden. Gebruikers ontvangen een melding als ze worden genoemd of als erop wordt geantwoord."
+ muted_tags_instructions: "Gebruikers ontvangen geen meldingen over nieuwe topics met deze tags en ze worden niet weergegeven in Nieuwste."
logs:
- title: "Logboeken"
+ title: "Logs"
when: "Wanneer"
action: "Actie"
acting_user: "Uitvoerende gebruiker"
@@ -719,28 +719,28 @@ nl:
title: "Toestemmingen"
none: "Er zijn geen categorieën aan deze groep gekoppeld."
description: "Leden van deze groep hebben toegang tot deze categorieën"
- public_admission: "Gebruikers mogen vrij aan de groep deelnemen (Vereist openbaar zichtbare groep)"
- public_exit: "Gebruikers mogen vrij de groep verlaten"
+ public_admission: "Gebruikers toestaan de groep te verlaten (vereist openbaar zichtbare groep)"
+ public_exit: "Gebruikers toestaan de groep te verlaten"
empty:
posts: "Er zijn geen berichten van leden van deze groep."
members: "Er zijn geen leden in deze groep."
- requests: "Er zijn geen lidmaatschapsaanvragen voor deze groep."
+ requests: "Er zijn geen lidmaatschapsverzoeken voor deze groep."
mentions: "Er zijn geen vermeldingen van deze groep."
messages: "Er zijn geen berichten voor deze groep."
topics: "Er zijn geen topics van leden van deze groep."
logs: "Er zijn geen logs voor deze groep."
add: "Toevoegen"
- join: "Toetreden"
+ join: "Deelnemen"
leave: "Verlaten"
- request: "Aanvraag"
+ request: "Verzoek"
message: "Bericht"
- confirm_leave: "Weet u zeker dat u deze groep wilt verlaten?"
- allow_membership_requests: "Gebruikers mogen lidmaatschapsaanvragen naar groepseigenaren sturen (Vereist openbaar zichtbare groep)"
- membership_request_template: "Aangepaste sjabloon om weer te geven voor gebruikers bij het sturen van een lidmaatschapsaanvraag"
+ confirm_leave: "Weet je zeker dat je deze groep wilt verlaten?"
+ allow_membership_requests: "Sta gebruikers toe om lidmaatschapsverzoeken naar groepseigenaren sturen (vereist een openbaar zichtbare groep)"
+ membership_request_template: "Aangepaste sjabloon om aan gebruikers te tonen bij het sturen van een lidmaatschapsverzoek"
membership_request:
- submit: "Aanvraag versturen"
+ submit: "Verzoek verzenden"
title: "Verzoek voor deelname aan @%{group_name}"
- reason: "Laat de groepseigenaren weten waarom u in deze groep hoort"
+ reason: "Laat de groepseigenaren weten waarom je in deze groep hoort"
membership: "Lidmaatschap"
name: "Naam"
group_name: "Groepsnaam"
@@ -753,7 +753,7 @@ nl:
all: "Alle groepen"
empty: "Er zijn geen zichtbare groepen."
filter: "Filteren op groepstype"
- owner_groups: "Mijn groepen"
+ owner_groups: "Groepen in mijn bezit"
close_groups: "Besloten groepen"
automatic_groups: "Automatische groepen"
automatic: "Automatisch"
@@ -774,35 +774,35 @@ nl:
filter_placeholder_admin: "gebruikersnaam of e-mailadres"
filter_placeholder: "gebruikersnaam"
remove_member: "Lid verwijderen"
- remove_member_description: "%{username} uit deze groep verwijderen"
+ remove_member_description: "Verwijder %{username} uit deze groep"
make_owner: "Eigenaar maken"
- make_owner_description: "%{username} een eigenaar van deze groep maken"
+ make_owner_description: "Maak %{username} een eigenaar van deze groep"
remove_owner: "Verwijderen als eigenaar"
- remove_owner_description: "%{username} als een eigenaar van deze groep verwijderen"
+ remove_owner_description: "Verwijder %{username} als eigenaar van deze groep"
make_primary: "Primair maken"
- make_primary_description: "Dit de primaire groep maken voor %{username}"
- remove_primary: "Als primaire verwijderen"
- remove_primary_description: "Deze als de primaire groep voor %{username} verwijderen"
+ make_primary_description: "Maak dit de primaire groep voor %{username}"
+ remove_primary: "Verwijderen als primair"
+ remove_primary_description: "Verwijder deze als primaire groep voor %{username}"
remove_members: "Leden verwijderen"
- remove_members_description: "Geselecteerde gebruikers verwijderen uit deze groep"
- make_owners: "Eigenaren maken"
- make_owners_description: "Geselecteerde gebruikers eigenaren van deze groep maken"
+ remove_members_description: "Verwijder de geselecteerde gebruikers uit deze groep"
+ make_owners: "Eigenaar maken"
+ make_owners_description: "Maak de geselecteerde gebruikers eigenaar van deze groep"
remove_owners: "Eigenaren verwijderen"
- remove_owners_description: "Geselecteerde gebruikers verwijderen als eigenaren van deze groep"
+ remove_owners_description: "Verwijder de geselecteerde gebruikers als eigenaar van deze groep"
make_all_primary: "Alles primair maken"
- make_all_primary_description: "Dit de primaire groep maken voor alle geselecteerde gebruikers."
- remove_all_primary: "Alles als primair verwijderen"
- remove_all_primary_description: "Deze groep verwijderen als primair"
+ make_all_primary_description: "Maak dit de primaire groep voor alle geselecteerde gebruikers."
+ remove_all_primary: "Verwijderen als primair"
+ remove_all_primary_description: "Verwijder deze groep als primair"
owner: "Eigenaar"
primary: "Primair"
- forbidden: "U mag de leden niet bekijken."
+ forbidden: "Je mag de leden niet bekijken."
topics: "Topics"
posts: "Berichten"
mentions: "Vermeldingen"
messages: "Berichten"
notification_level: "Standaard meldingsniveau voor groepsberichten"
alias_levels:
- mentionable: "Wie kan deze groep taggen?"
+ mentionable: "Wie kan deze groep @noemen?"
messageable: "Wie kan deze groep een bericht sturen?"
nobody: "Niemand"
only_admins: "Alleen beheerders"
@@ -812,39 +812,39 @@ nl:
everyone: "Iedereen"
notifications:
watching:
- title: "In de gaten houden"
- description: "U ontvangt een melding bij elk nieuw bericht, en het aantal nieuwe antwoorden wordt weergeven."
+ title: "Geobserveerd"
+ description: "Je ontvangt een melding bij elk nieuw bericht en het aantal nieuwe antwoorden wordt weergeven."
watching_first_post:
- title: "Eerste bericht in de gaten houden"
- description: "U ontvangt meldingen van nieuwe berichten in deze groep, maar niet van antwoorden op de berichten."
+ title: "Eerste bericht geobserveerd"
+ description: "Je ontvangt meldingen van nieuwe berichten in deze groep, maar niet van antwoorden op de berichten."
tracking:
title: "Volgen"
- description: "U ontvangt een melding als iemand uw @naam noemt of een bericht van u beantwoordt, en het aantal nieuwe antwoorden wordt weergeven."
+ description: "Je ontvangt een melding als iemand je @naam noemt of een bericht van je beantwoordt en het aantal nieuwe antwoorden wordt weergeven."
regular:
title: "Normaal"
- description: "U ontvangt een melding als iemand uw @naam noemt of een bericht van u beantwoordt."
+ description: "Je ontvangt een melding als iemand je @naam noemt of een bericht van je beantwoordt."
muted:
- title: "Genegeerd"
- description: "U ontvangt geen enkele melding over berichten in deze groep."
- flair_url: "Afbeelding voor avatar-flair"
+ title: "Gedempt"
+ description: "Je ontvangt geen meldingen over berichten in deze groep."
+ flair_url: "Afbeelding voor avatarflair"
flair_upload_description: "Gebruik vierkante afbeeldingen, niet kleiner dan 20px bij 20px."
- flair_bg_color: "Achtergrondkleur van avatar-flair"
- flair_bg_color_placeholder: "(Optioneel) Hex-kleurwaarde"
- flair_color: "Kleur van avatar-flair"
- flair_color_placeholder: "(Optioneel) Hex-kleurwaarde"
+ flair_bg_color: "Achtergrondkleur van avatarflair"
+ flair_bg_color_placeholder: "(Optioneel) Hexadecimale kleurwaarde"
+ flair_color: "Kleur van avatarflair"
+ flair_color_placeholder: "(Optioneel) Hexadecimale kleurwaarde"
flair_preview_icon: "Pictogramvoorbeeld"
- flair_preview_image: "Afbeeldingsvoorbeeld"
+ flair_preview_image: "Voorbeeld van afbeelding"
flair_type:
- icon: "Een pictogram selecteren"
- image: "Een afbeelding uploaden"
+ icon: "Selecteer een pictogram"
+ image: "Upload een afbeelding"
default_notifications:
- modal_description: "Wilt u deze wijziging op het verleden toepassen? Hierdoor worden voor %{count} bestaande gebruikers voorkeuren gewijzigd."
+ modal_description: "Wil je deze wijziging met terugwerkende kracht toepassen? Hierdoor worden voorkeuren gewijzigd voor %{count} bestaande gebruikers."
modal_yes: "Ja"
modal_no: "Nee, wijziging alleen vanaf nu toepassen"
user_action_groups:
- "1": "Gegeven likes"
- "2": "Ontvangen likes"
- "3": "Favorieten"
+ "1": "Likes"
+ "2": "Likes"
+ "3": "Bladwijzers"
"4": "Topics"
"5": "Antwoorden"
"6": "Reacties"
@@ -853,17 +853,17 @@ nl:
"11": "Bewerkingen"
"12": "Verzonden items"
"13": "Inbox"
- "14": "In wachtrij"
+ "14": "Wachtend"
"15": "Concepten"
categories:
all: "alle categorieën"
all_subcategories: "alles"
no_subcategory: "geen"
category: "Categorie"
- category_list: "Categorielijst weergeven"
+ category_list: "Categorieënlijst weergeven"
reorder:
- title: "Categorieën herschikken"
- title_long: "De categorielijst opnieuw ordenen"
+ title: "Categorieën herordenen"
+ title_long: "Categorieënlijst herordenen"
save: "Volgorde opslaan"
apply_all: "Toepassen"
position: "Positie"
@@ -871,7 +871,7 @@ nl:
topics: "Topics"
latest: "Nieuwste"
subcategories: "Subcategorieën"
- muted: "Genegeerde categorieën"
+ muted: "Gedempte categorieën"
topic_sentence:
one: "%{count} topic"
other: "%{count} topics"
@@ -885,11 +885,11 @@ nl:
one: "%{number} totaal"
other: "%{number} totaal"
topic_stat_sentence_week:
- one: "%{count} nieuw topic in de afgelopen week."
- other: "%{count} nieuwe topics in de afgelopen week."
+ one: "%{count} nieuw topic de afgelopen week."
+ other: "%{count} nieuwe topics de afgelopen week."
topic_stat_sentence_month:
- one: "%{count} nieuw topic in de afgelopen maand."
- other: "%{count} nieuwe topics in de afgelopen maand."
+ one: "%{count} nieuw topic de afgelopen maand."
+ other: "%{count} nieuwe topics de afgelopen maand."
n_more: "Categorieën (nog %{count})..."
ip_lookup:
title: IP-adres zoeken
@@ -897,16 +897,16 @@ nl:
location: Locatie
location_not_found: (onbekend)
organisation: Organisatie
- phone: Telefoon
+ phone: Telefoonnummer
other_accounts: "Andere accounts met dit IP-adres:"
delete_other_accounts: "%{count} verwijderen"
username: "gebruikersnaam"
trust_level: "TL"
read_time: "leestijd"
topics_entered: "topics ingevoerd"
- post_count: "# berichten"
- confirm_delete_other_accounts: "Weet u zeker dat u deze accounts wilt verwijderen?"
- powered_by: "gebruikt MaxMindDB"
+ post_count: "Aantal berichten"
+ confirm_delete_other_accounts: "Weet je zeker dat je deze accounts wilt verwijderen?"
+ powered_by: "met MaxMindDB"
copied: "gekopieerd"
user_fields:
none: "(selecteer een optie)"
@@ -914,70 +914,70 @@ nl:
user:
said: "%{username}:"
profile: "Profiel"
- mute: "Negeren"
+ mute: "Dempen"
edit: "Voorkeuren bewerken"
download_archive:
button_text: "Alles downloaden"
- confirm: "Weet u zeker dat u uw berichten wilt downloaden?"
- success: "Downloaden is gestart; u ontvangt een melding zodra het proces is voltooid."
- rate_limit_error: "Berichten kunnen maar één keer per dag worden gedownload; probeer het morgen opnieuw."
+ confirm: "Weet je zeker dat je je berichten wilt downloaden?"
+ success: "Download is gestart, je ontvangt een melding zodra het proces is voltooid."
+ rate_limit_error: "Berichten kunnen één keer per dag worden gedownload. Probeer het morgen opnieuw."
new_private_message: "Nieuw bericht"
private_message: "Bericht"
private_messages: "Berichten"
user_notifications:
filters:
filter_by: "Filteren op"
- all: "Alle"
+ all: "Alles"
read: "Gelezen"
unread: "Ongelezen"
- unseen: "Ongelezen"
+ unseen: "Ongezien"
ignore_duration_title: "Gebruiker negeren"
ignore_duration_username: "Gebruikersnaam"
- ignore_duration_when: "Tijdsduur:"
+ ignore_duration_when: "Duur:"
ignore_duration_save: "Negeren"
- ignore_duration_note: "Houd er rekening mee dat alle negeeracties na het verlopen van de tijdsduur automatisch worden verwijderd."
+ ignore_duration_note: "Houd er rekening mee dat alle negeeracties na het verlopen van de negeerduur automatisch worden verwijderd."
ignore_duration_time_frame_required: "Selecteer een tijdsbestek"
- ignore_no_users: "U hebt geen genegeerde gebruikers."
+ ignore_no_users: "Je hebt geen genegeerde gebruikers."
ignore_option: "Genegeerd"
- ignore_option_title: "U ontvangt geen meldingen met betrekking tot deze gebruiker, en alle topics en antwoorden ervan worden verborgen."
+ ignore_option_title: "Je ontvangt geen meldingen met betrekking tot deze gebruiker en alle topics en antwoorden ervan worden verborgen."
add_ignored_user: "Toevoegen..."
mute_option: "Gedempt"
- mute_option_title: "U ontvangt geen meldingen met betrekking tot deze gebruiker."
+ mute_option_title: "Je ontvangt geen meldingen met betrekking tot deze gebruiker."
normal_option: "Normaal"
- normal_option_title: "U ontvangt een melding als deze gebruiker een bericht van u beantwoordt, u citeert, of uw naam noemt."
+ normal_option_title: "Je ontvangt een melding als deze gebruiker een bericht van je beantwoordt, je citeert, of je naam noemt."
notification_schedule:
title: "Meldingsschema"
label: "Aangepast meldingsschema inschakelen"
- tip: "Buiten deze uren wordt u automatisch op 'niet storen' gezet."
+ tip: "Buiten deze tijden word je automatisch op 'niet storen' gezet."
midnight: "Middernacht"
none: "Geen"
monday: "Maandag"
tuesday: "Dinsdag"
- wednesday: "woensdag"
- thursday: "donderdag"
- friday: "vrijdag"
- saturday: "zaterdag"
- sunday: "zondag"
- to: "aan"
+ wednesday: "Woensdag"
+ thursday: "Donderdag"
+ friday: "Vrijdag"
+ saturday: "Zaterdag"
+ sunday: "Zondag"
+ to: "tot"
activity_stream: "Activiteit"
read: "Gelezen"
- read_help: "Onlangs gelezen topics"
+ read_help: "Recent gelezen topics"
preferences: "Voorkeuren"
feature_topic_on_profile:
open_search: "Selecteer een nieuw topic"
- title: "Een topic selecteren"
- search_label: "Zoeken naar topic op titel"
+ title: "Selecteer een topic"
+ search_label: "Topic zoeken op titel"
save: "Opslaan"
clear:
title: "Wissen"
- warning: "Weet u zeker dat u uw aanbevolen topic wilt wissen?"
+ warning: "Weet je zeker dat je je uitgelichte topic wilt wissen?"
use_current_timezone: "Huidige tijdzone gebruiken"
profile_hidden: "Het openbare profiel van deze gebruiker is verborgen."
expand_profile: "Uitvouwen"
sr_expand_profile: "Profieldetails uitvouwen"
collapse_profile: "Samenvouwen"
- sr_collapse_profile: "Profieldetails inklappen"
- bookmarks: "Favorieten"
+ sr_collapse_profile: "Profieldetails samenvouwen"
+ bookmarks: "Bladwijzers"
bio: "Over mij"
timezone: "Tijdzone"
invited_by: "Uitgenodigd door"
@@ -986,41 +986,41 @@ nl:
statistics: "Statistieken"
desktop_notifications:
label: "Livemeldingen"
- not_supported: "Meldingen worden in deze browser niet ondersteund. Sorry."
+ not_supported: "Meldingen worden niet ondersteund in deze browser. Sorry."
perm_default: "Meldingen inschakelen"
perm_denied_btn: "Toestemming geweigerd"
- perm_denied_expl: "U hebt toestemming voor meldingen geweigerd. Sta meldingen toe via uw browserinstellingen."
+ perm_denied_expl: "Je hebt toestemming voor meldingen geweigerd. Sta meldingen toe via je browserinstellingen."
disable: "Meldingen uitschakelen"
enable: "Meldingen inschakelen"
- each_browser_note: 'Opmerking: U moet deze instelling wijzigen in elke browser die u gebruikt. Alle meldingen worden uitgeschakeld wanneer u zich in "niet storen" bevindt, ongeacht deze instelling.'
- consent_prompt: "Wilt u livemeldingen ontvangen als mensen op uw berichten antwoorden?"
+ each_browser_note: 'Opmerking: je moet deze instelling wijzigen in elke browser die je gebruikt. Alle meldingen worden uitgeschakeld wanneer je op ''niet storen'' staat, ongeacht deze instelling.'
+ consent_prompt: "Wil je live meldingen ontvangen als mensen antwoorden op je berichten?"
dismiss: "Negeren"
- dismiss_notifications: "Alle verwijderen"
+ dismiss_notifications: "Alles negeren"
dismiss_notifications_tooltip: "Alle ongelezen meldingen markeren als gelezen"
- no_messages_title: "U hebt geen berichten"
+ no_messages_title: "Je hebt geen berichten"
no_messages_body: >
- Wilt u een direct persoonlijk gesprek met iemand hebben, buiten de normale gespreksstroom? Stuur ze een bericht door hun avatar te selecteren en de %{icon} berichtknop te gebruiken.
Als u hulp nodig hebt, kunt u een medewerker een bericht sturen.
- no_bookmarks_title: "U hebt nog geen bladwijzers gemaakt"
+ Wil je een direct persoonlijk gesprek met iemand hebben, buiten de normale gespreksstroom? Stuur de persoon een bericht door diens avatar te selecteren en de %{icon} berichtknop te gebruiken.
Als je hulp nodig hebt, kun je een medewerker een bericht sturen.
+ no_bookmarks_title: "Je hebt nog geen bladwijzers gemaakt"
no_bookmarks_body: >
- Begin een bladwijzer te maken voor berichten met de %{icon} knop en ze worden hier weergegeven voor gemakkelijke referentie. U kunt ook een herinnering plannen!
- no_bookmarks_search: "Geen bladwijzers gevonden met de opgegeven zoekopdracht."
- no_notifications_title: "U hebt nog geen meldingen"
- no_notifications_page_title: "U hebt nog geen meldingen"
+ Begin met bladwijzers voor berichten te maken met de %{icon} knop. Ze worden hier weergegeven voor eenvoudige referentie. Je kunt ook een herinnering plannen!
+ no_bookmarks_search: "Geen bladwijzers gevonden met de opgegeven zoekcriteria."
+ no_notifications_title: "Je hebt nog geen meldingen"
+ no_notifications_page_title: "Je hebt nog geen meldingen"
first_notification: "Uw eerste melding! Selecteer deze om te beginnen."
- dynamic_favicon: "Aantal op browserpictogram tonen"
+ dynamic_favicon: "Aantallen weergeven op browserpictogram"
skip_new_user_tips:
- description: "Onboarding-tips en badges voor nieuwe gebruikers overslaan"
- not_first_time: "Niet uw eerste keer?"
+ description: "Introductietips en badges voor nieuwe gebruikers overslaan"
+ not_first_time: "Niet je eerste keer?"
skip_link: "Deze tips overslaan"
- read_later: "Ik zal het later lezen."
+ read_later: "Ik lees het later."
theme_default_on_all_devices: "Dit het standaardthema maken op al mijn apparaten"
- color_scheme_default_on_all_devices: "Standaard kleurenschema(’s) op al mijn apparaten instellen"
+ color_scheme_default_on_all_devices: "Standaard kleurenschema(’s) instellen op al mijn apparaten"
color_scheme: "Kleurenschema"
color_schemes:
default_description: "Standaard voor thema"
disable_dark_scheme: "Hetzelfde als normaal"
- dark_instructions: "U kunt een voorbeeld van het kleurenschema van de donkere modus bekijken door de donkere modus van uw apparaat om te schakelen."
- undo: "Terugzetten"
+ dark_instructions: "Je kunt een voorbeeld van het kleurenschema van de donkere modus bekijken door de donkere modus van je apparaat in te schakelen."
+ undo: "Herstellen"
regular: "Normaal"
dark: "Donkere modus"
default_dark_scheme: "(standaard voor website)"
@@ -1028,15 +1028,14 @@ nl:
dark_mode_enable: "Automatisch kleurenschema voor donkere modus inschakelen"
text_size_default_on_all_devices: "Dit de standaard tekstgrootte maken op al mijn apparaten"
allow_private_messages: "Andere gebruikers mogen mij persoonlijke berichten sturen"
- external_links_in_new_tab: "Alle externe koppelingen openen in een nieuw tabblad"
- enable_quoting: "Antwoord-met-citaat voor gemarkeerde tekst inschakelen"
- enable_defer: "Negeren voor markeren van topics als ongelezen inschakelen"
+ external_links_in_new_tab: "Alle externe links openen op een nieuw tabblad"
+ enable_quoting: "Antwoorden met citaat inschakelen voor gemarkeerde tekst"
+ enable_defer: "Uitstellen door als ongelezen te markeren inschakelen"
experimental_sidebar:
options: "Opties"
navigation_section: "Navigatie"
- list_destination_default: "Standaard"
change: "wijzigen"
- featured_topic: "Aanbevolen topic"
+ featured_topic: "Uitgelicht topic"
moderator: "%{user} is een moderator"
admin: "%{user} is een beheerder"
moderator_tooltip: "Deze gebruiker is een moderator"
@@ -1052,60 +1051,60 @@ nl:
enabled: "Mailinglijstmodus inschakelen"
instructions: |
Deze instelling overschrijft de activiteitsamenvatting.
- Genegeerde topics en categorieën zijn niet in deze e-mails inbegrepen.
- individual: "Een e-mail voor elk nieuw bericht verzenden"
- individual_no_echo: "Een e-mail voor elk nieuw bericht verzenden, behalve die van mezelf"
- many_per_day: "Mij een e-mail voor elk nieuw bericht sturen (ongeveer %{dailyEmailEstimate} per dag)"
- few_per_day: "Mij een e-mail voor elk nieuw bericht sturen (ongeveer 2 per dag)"
+ Gedempte topics en categorieën zijn niet opgenomen in deze e-mails.
+ individual: "E-mail sturen voor elk nieuw bericht"
+ individual_no_echo: "E-mail sturen voor elk nieuw bericht, behalve die van mezelf"
+ many_per_day: "Stuur mij een e-mail voor elk nieuw bericht (ongeveer %{dailyEmailEstimate} per dag)"
+ few_per_day: "Stuur mij een e-mail voor elk nieuw bericht (ongeveer 2 per dag)"
warning: "Mailinglijstmodus ingeschakeld. E-mailmeldingsinstellingen worden genegeerd."
tag_settings: "Tags"
- watched_tags: "In de gaten gehouden"
- watched_tags_instructions: "U houdt automatisch alle nieuwe topics met deze tags in de gaten. U ontvangt meldingen bij alle nieuwe berichten en topics, en het aantal nieuwe berichten verschijnt ook naast het topic."
+ watched_tags: "Geobserveerd"
+ watched_tags_instructions: "Je observeert automatisch alle nieuwe topics met deze tags. Je ontvangt meldingen bij alle nieuwe berichten en topics en het aantal nieuwe berichten wordt weergegeven naast het topic."
tracked_tags: "Gevolgd"
- tracked_tags_instructions: "U volgt automatisch alle topics met deze tags. Het aantal nieuwe berichten verschijnt naast het topic."
- muted_tags: "Genegeerd"
- muted_tags_instructions: "U ontvangt geen enkele melding over nieuwe topics met deze tags, en ze verschijnen niet in Nieuwste."
- watched_categories: "In de gaten gehouden"
- watched_categories_instructions: "U houdt automatisch alle nieuwe topics in deze categorieën in de gaten. U ontvangt meldingen bij alle nieuwe berichten en topics, en het aantal nieuwe berichten verschijnt ook naast het topic."
+ tracked_tags_instructions: "Je volgt automatisch alle topics met deze tags. Het aantal nieuwe berichten wordt weergegeven naast het topic."
+ muted_tags: "Gedempt"
+ muted_tags_instructions: "Je ontvangt geen meldingen over nieuwe topics met deze tags en deze worden niet weergegeven in Nieuwste."
+ watched_categories: "Geobserveerd"
+ watched_categories_instructions: "Je observeert automatisch alle nieuwe topics in deze categorieën. Je ontvangt meldingen bij alle nieuwe berichten en topics en het aantal nieuwe berichten wordt weergegeven naast het topic."
tracked_categories: "Gevolgd"
- tracked_categories_instructions: "U volgt automatisch alle topics in deze categorieën. Het aantal nieuwe berichten verschijnt naast het topic."
- watched_first_post_categories: "Eerste bericht in de gaten houden."
- watched_first_post_categories_instructions: "U ontvangt een melding bij het eerste bericht in elk nieuw topic in deze categorieën."
- watched_first_post_tags: "Eerste bericht in de gaten houden"
- watched_first_post_tags_instructions: "U ontvangt een melding bij het eerste bericht in elk nieuw topic met deze tags."
- muted_categories: "Genegeerd"
- muted_categories_instructions: "U ontvangt geen enkele melding over nieuwe topics en berichten in deze categorieën, en ze verschijnen niet op de pagina's Categorieën of Nieuwste."
- muted_categories_instructions_dont_hide: "U ontvangt geen enkele melding over nieuwe topics in deze categorieën."
+ tracked_categories_instructions: "Je volgt automatisch alle topics in deze categorieën. Het aantal nieuwe berichten wordt weergegeven naast het topic."
+ watched_first_post_categories: "Eerste bericht geobserveerd"
+ watched_first_post_categories_instructions: "Je ontvangt een melding bij het eerste bericht in elk nieuw topic in deze categorieën."
+ watched_first_post_tags: "Eerste bericht geobserveerd"
+ watched_first_post_tags_instructions: "Je ontvangt een melding bij het eerste bericht in elk nieuw topic met deze tags."
+ muted_categories: "Gedempt"
+ muted_categories_instructions: "Je ontvangt geen meldingen over nieuwe topics en berichten in deze categorieën en deze worden niet weergegeven op de pagina's Categorieën of Nieuwste."
+ muted_categories_instructions_dont_hide: "Je ontvangt geen meldingen over nieuwe topics in deze categorieën."
regular_categories: "Normaal"
- regular_categories_instructions: "Deze categorieën ziet u in de topiclijsten ‘Nieuwste’ en ‘Top’."
- no_category_access: "Als moderator hebt u beperkte toegang tot categorieën, opslaan is uitgeschakeld."
+ regular_categories_instructions: "Deze categorieën zie je in de topiclijsten 'Nieuwste’ en ‘Top’."
+ no_category_access: "Als moderator heb je beperkte toegang tot categorieën, opslaan is uitgeschakeld."
delete_account: "Mijn account verwijderen"
- delete_account_confirm: "Weet u zeker dat u uw account definitief wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt!"
- deleted_yourself: "Uw account is succesvol verwijderd."
- delete_yourself_not_allowed: "Neem contact op met een staflid als u wilt dat uw account wordt verwijderd."
+ delete_account_confirm: "Weet je zeker dat je je account definitief wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt!"
+ deleted_yourself: "Uw account is verwijderd."
+ delete_yourself_not_allowed: "Neem contact op met een staflid als je wilt dat je account wordt verwijderd."
unread_message_count: "Berichten"
admin_delete: "Verwijderen"
users: "Gebruikers"
- muted_users: "Genegeerd"
- muted_users_instructions: "Alle meldingen en PB's van deze gebruikers onderdrukken."
+ muted_users: "Gedempt"
+ muted_users_instructions: "Onderdruk alle meldingen en PB's van deze gebruikers."
allowed_pm_users: "Toegestaan"
- allowed_pm_users_instructions: "Alleen PB's van deze gebruikers toestaan."
+ allowed_pm_users_instructions: "Sta alleen PB's van deze gebruikers toe."
allow_private_messages_from_specific_users: "Alleen bepaalde gebruikers mogen mij persoonlijke berichten sturen"
ignored_users: "Genegeerd"
- ignored_users_instructions: "Alle berichten, meldingen en PB's van deze gebruikers onderdrukken."
- tracked_topics_link: "Tonen"
- automatically_unpin_topics: "Topics automatisch losmaken als ik de onderkant bereik"
+ ignored_users_instructions: "Onderdruk alle berichten, meldingen en PB's van deze gebruikers."
+ tracked_topics_link: "Weergeven"
+ automatically_unpin_topics: "Topics automatisch losmaken als ik de onderkant bereik."
apps: "Apps"
revoke_access: "Toegang intrekken"
undo_revoke_access: "Toegang intrekken ongedaan maken"
api_approved: "Goedgekeurd:"
api_last_used_at: "Laatst gebruikt op:"
theme: "Thema"
- save_to_change_theme: 'Thema wordt bijgewerkt nadat u op ''%{save_text}'' klikt'
+ save_to_change_theme: 'Thema wordt bijgewerkt nadat je op ''%{save_text}'' klikt'
home: "Standaard startpagina"
- staged: "Staged"
+ staged: "Gefaseerd"
staff_counters:
- flags_given: "behulpzame markeringen"
+ flags_given: "nuttige markeringen"
flagged_posts: "gemarkeerde berichten"
deleted_posts: "verwijderde berichten"
suspensions: "schorsingen"
@@ -1113,7 +1112,7 @@ nl:
rejected_posts: "geweigerde berichten"
messages:
all: "alle inboxen"
- inbox: "Postvak IN"
+ inbox: "Inbox"
personal: "Persoonlijk"
latest: "Nieuwste"
sent: "Verzonden"
@@ -1127,9 +1126,9 @@ nl:
other: "Nieuw (%{count})"
archive: "Archief"
groups: "Mijn groepen"
- move_to_inbox: "Verplaatsen naar Postvak IN"
+ move_to_inbox: "Verplaatsen naar inbox"
move_to_archive: "Archiveren"
- failed_to_move: "Het verplaatsen van geselecteerde berichten is niet gelukt (misschien is uw netwerkverbinding verbroken)"
+ failed_to_move: "Verplaatsen van geselecteerde berichten mislukt (mogelijk is je netwerkverbinding verbroken)"
tags: "Tags"
all_tags: "Alle tags"
warnings: "Officiële waarschuwingen"
@@ -1146,31 +1145,31 @@ nl:
apps: "Apps"
change_password:
success: "(e-mail verzonden)"
- in_progress: "(e-mail wordt verzonden)"
+ in_progress: "(e-mail verzenden)"
error: "(fout)"
emoji: "slot-emoji"
- action: "E-mail voor wachtwoordherinitialisatie verzenden"
+ action: "E-mail voor wachtwoordherstel verzenden"
set_password: "Wachtwoord instellen"
choose_new: "Kies een nieuw wachtwoord"
choose: "Kies een wachtwoord"
second_factor_backup:
- title: "Tweefactor-back-upcodes"
+ title: "Back-upcodes voor tweeledige verificatie"
regenerate: "Opnieuw genereren"
disable: "Uitschakelen"
enable: "Inschakelen"
enable_long: "Back-upcodes inschakelen"
manage:
- one: "Back-upcodes beheren. U hebt %{count} back-upcode over."
- other: "Back-upcodes beheren. U hebt %{count} back-upcodes over."
+ one: "Back-upcodes beheren. Je hebt nog %{count} back-upcode over."
+ other: "Back-upcodes beheren. Je hebt nog %{count} back-upcodes over."
copy_to_clipboard: "Kopiëren naar klembord"
copy_to_clipboard_error: "Fout bij kopiëren van gegevens naar klembord"
copied_to_clipboard: "Gekopieerd naar klembord"
download_backup_codes: "Back-upcodes downloaden"
remaining_codes:
- one: "U hebt %{count} back-upcode over."
- other: "U hebt %{count} back-upcodes over."
+ one: "Je hebt nog %{count} back-upcode over."
+ other: "Je hebt nog %{count} back-upcodes over."
use: "Een back-upcode gebruiken"
- enable_prerequisites: "U moet een primaire tweefactormethode inschakelen voordat u back-upcodes genereert."
+ enable_prerequisites: "Je moet een primaire methode voor tweeledige verificatie inschakelen voordat je back-upcodes genereert."
codes:
title: "Back-upcodes gegenereerd"
description: "Elke back-upcode kan maar één keer worden gebruikt. Bewaar ze op een veilige maar toegankelijke plek."
@@ -1182,7 +1181,7 @@ nl:
confirm_password_description: "Bevestig uw wachtwoord om door te gaan"
name: "Naam"
label: "Code"
- rate_limit: "Wacht even voordat u een andere authenticatiecode probeert."
+ rate_limit: "Wacht even voordat je een andere verificatiecode probeert."
enable_description: |
Scan deze QR-code in een ondersteunde app (Android – iOS) en voer uw authenticatiecode in.
disable_description: "Voer de authenticatiecode van uw app in"
@@ -1193,9 +1192,9 @@ nl:
Tweefactorauthenticatie voegt extra beveiliging toe aan uw account door naast uw wachtwoord een eenmalige code te vereisen. Tokens kunnen op Android- en iOS -apparaten worden gegenereerd.
oauth_enabled_warning: "Houd er rekening mee dat sociale aanmeldingen worden uitgeschakeld zodra tweefactorauthenticatie op uw account is ingeschakeld."
use: "Authenticator-app gebruiken"
- enforced_notice: "U dient tweefactorauthenticatie in te schakelen voordat u deze website bezoekt."
+ enforced_notice: "Je moet tweeledige verificatie inschakelen voordat je deze website bezoekt."
disable: "Uitschakelen"
- disable_confirm: "Weet u zeker dat u alle tweefactormethoden wilt uitschakelen?"
+ disable_confirm: "Weet je zeker dat je alle methoden voor tweeledige verificatie wilt uitschakelen?"
save: "Opslaan"
edit: "Bewerken"
edit_title: "Authenticator bewerken"
@@ -1204,81 +1203,81 @@ nl:
title: "Op tokens gebaseerde authenticators"
add: "Authenticator toevoegen"
default_name: "Mijn authenticator"
- name_and_code_required_error: "U moet een naam en de code van uw authenticator-app opgeven."
+ name_and_code_required_error: "Je moet een naam en de code van je authenticator-app opgeven."
security_key:
register: "Registreren"
default_name: "Hoofdbeveiligingssleutel"
iphone_default_name: "iPhone"
android_default_name: "Android"
not_allowed_error: "Het registratieproces van de beveiligingssleutel had een time-out of is geannuleerd."
- already_added_error: "U hebt deze beveiligingssleutel al geregistreerd. U hoeft deze niet opnieuw te registreren."
+ already_added_error: "Je hebt deze beveiligingssleutel al geregistreerd. Je hoeft deze niet opnieuw te registreren."
save: "Opslaan"
- name_required_error: "U moet een naam voor uw beveiligingssleutel opgeven."
+ name_required_error: "Je moet een naam voor je beveiligingssleutel opgeven."
change_about:
title: "Over mij wijzigen"
error: "Er is een fout opgetreden bij het wijzigen van deze waarde."
change_username:
title: "Gebruikersnaam wijzigen"
- confirm: "Weet u absoluut zeker dat u uw gebruikersnaam wilt wijzigen?"
- taken: "Sorry, maar die gebruikersnaam is al in gebruik."
- invalid: "Die gebruikersnaam is ongeldig. Hij mag alleen cijfers en letters bevatten."
+ confirm: "Weet je zeker dat je je gebruikersnaam wilt wijzigen?"
+ taken: "Sorry, die gebruikersnaam is al in gebruik."
+ invalid: "Die gebruikersnaam is ongeldig. De naam mag alleen cijfers en letters bevatten."
add_email:
title: "E-mailadres toevoegen"
add: "toevoegen"
change_email:
title: "E-mailadres wijzigen"
taken: "Sorry, dat e-mailadres is niet beschikbaar."
- error: "Er is een fout opgetreden bij het wijzigen van uw e-mailadres. Misschien is dat adres al in gebruik?"
- success: "We hebben een e-mail naar dat adres gestuurd. Volg de instructies voor bevestiging."
- success_via_admin: "We hebben een e-mail naar dat adres gestuurd. Volg de instructies voor bevestiging in de e-mail."
- success_staff: "Er is een e-mail naar uw huidige adres verzonden. Volg de bevestigingsinstructies."
+ error: "Er is een fout opgetreden bij het wijzigen van je e-mailadres. Misschien is het adres al in gebruik?"
+ success: "We hebben een e-mail naar dat adres gestuurd. Volg de bevestigingsinstructies."
+ success_via_admin: "We hebben een e-mail naar dat adres gestuurd. Volg de bevestigingsinstructies in de e-mail."
+ success_staff: "We hebben een e-mail naar je huidige adres gestuurd. Volg de bevestigingsinstructies."
change_avatar:
title: "Uw profielafbeelding wijzigen"
gravatar: "%{gravatarName}, gebaseerd op"
- gravatar_title: "Wijzig uw avatar op de website van %{gravatarName}"
- gravatar_failed: "We konden geen %{gravatarName} voor dat e-mailadres vinden."
+ gravatar_title: "Wijzig je avatar op de website van %{gravatarName}"
+ gravatar_failed: "We konden geen %{gravatarName} vinden voor dat e-mailadres."
refresh_gravatar_title: "Uw %{gravatarName} vernieuwen"
letter_based: "Door systeem toegekende profielafbeelding"
uploaded_avatar: "Eigen afbeelding"
- uploaded_avatar_empty: "Een eigen afbeelding toevoegen"
- upload_title: "Uw afbeelding uploaden"
- image_is_not_a_square: "Waarschuwing: we hebben uw afbeelding bijgesneden; breedte en hoogte waren niet gelijk."
+ uploaded_avatar_empty: "Voeg een eigen afbeelding toe"
+ upload_title: "Upooad je afbeelding"
+ image_is_not_a_square: "Waarschuwing: we hebben je afbeelding bijgesneden; de breedte en hoogte waren niet gelijk."
logo_small: "Het kleine logo van de website. Standaard gebruikt."
change_profile_background:
title: "Profielkoptekst"
- instructions: "Profielkopteksten worden gecentreerd en hebben een standaardbreedte van 1110px."
+ instructions: "Profielkopteksten worden gecentreerd en hebben een standaardbreedte van 1110 pixels."
change_card_background:
title: "Achtergrond van gebruikerskaart"
- instructions: "Achtergrondafbeeldingen worden gecentreerd en hebben een standaardbreedte van 590px."
+ instructions: "Achtergrondafbeeldingen worden gecentreerd en hebben een standaardbreedte van 590 pixels."
change_featured_topic:
- title: "Aanbevolen topic"
- instructions: "Er wordt een koppeling naar dit topic op uw gebruikerskaart en profiel geplaatst."
+ title: "Uitgelicht topic"
+ instructions: "Er wordt een link naar dit topic op je gebruikerskaart en profiel geplaatst."
email:
- title: "E-mail"
+ title: "E-mailadres"
primary: "Primair e-mailadres"
- secondary: "Extra e-mailadressen"
- primary_label: "primaire"
+ secondary: "Secundaire e-mailadressen"
+ primary_label: "primair"
unconfirmed_label: "onbevestigd"
- resend_label: "bevestigingsmail opnieuw verzenden"
+ resend_label: "bevestigings-e-mail opnieuw verzenden"
resending_label: "verzenden..."
resent_label: "e-mail verzonden"
update_email: "E-mailadres wijzigen"
set_primary: "Primair e-mailadres instellen"
destroy: "E-mailadres verwijderen"
add_email: "Alternatief e-mailadres toevoegen"
- auth_override_instructions: "E-mailadres kan worden bijgewerkt vanaf de authenticatieprovider."
- no_secondary: "Geen extra e-mailadressen"
+ auth_override_instructions: "E-mailadres kan worden bijgewerkt via de authenticatieprovider."
+ no_secondary: "Geen secundaire e-mailadressen"
instructions: "Nooit openbaar zichtbaar."
- admin_note: "Opmerking: een beheerder die het e-mailadres van een andere niet-beheerder wijzigt, geeft aan dat de gebruiker geen toegang meer heeft tot zijn of haar e-mailaccount, dus er wordt een e-mail voor opnieuw instellen van het wachtwoord naar zijn of haar nieuwe adres gestuurd. Het e-mailadres van de gebruiker wordt pas gewijzigd nadat hij of zij het proces voor opnieuw instellen van het wachtwoord heeft voltooid."
+ admin_note: "Opmerking: een beheerder die het e-mailadres van een andere niet-beheerder wijzigt, geeft aan dat de gebruiker geen toegang meer heeft tot diens e-mailaccount, dus wordt er een e-mail voor wachtwoordherstel naar diens nieuwe adres gestuurd. Het e-mailadres van de gebruiker wordt pas gewijzigd nadat deze het proces voor wachtwoordherstel heeft voltooid."
ok: "We sturen een e-mail ter bevestiging"
required: "Voer een e-mailadres in"
invalid: "Voer een geldig e-mailadres in"
- authenticated: "Uw e-mailadres is geauthenticeerd door %{provider}"
- invite_auth_email_invalid: "Uw uitnodigingsmail komt niet overeen met het e-mailadres dat door %{provider} is geverifieerd"
- frequency_immediately: "Als u de inhoud in kwestie nog niet hebt gelezen, sturen we u direct een e-mail."
+ authenticated: "Uw e-mailadres is geverifieerd door %{provider}"
+ invite_auth_email_invalid: "Uw uitnodigings-e-mail komt niet overeen met het e-mailadres dat door %{provider} is geverifieerd"
+ frequency_immediately: "Als je de inhoud in kwestie nog niet hebt gelezen, sturen we je direct een e-mail."
frequency:
- one: "We sturen alleen een e-mail als we u de laatste minuut niet hebben gezien."
- other: "We sturen alleen een e-mail als we u de laatste %{count} minuten niet hebben gezien."
+ one: "We sturen alleen een e-mail als we je de afgelopen minuut niet hebben gezien."
+ other: "We sturen alleen een e-mail als we je de afgelopen %{count} minuten niet hebben gezien."
associated_accounts:
title: "Gekoppelde accounts"
connect: "Verbinden"
@@ -1287,8 +1286,8 @@ nl:
not_connected: "(niet gekoppeld)"
confirm_modal_title: "%{provider}-account koppelen"
confirm_description:
- account_specific: "Uw %{provider}-account '%{account_description}' wordt voor authenticatie gebruikt."
- generic: "Uw %{provider}-account wordt voor authenticatie gebruikt."
+ account_specific: "Uw %{provider}-account '%{account_description}' wordt gebruikt voor verificatie."
+ generic: "Uw %{provider}-account wordt gebruikt voor verificatie."
name:
title: "Naam"
instructions: "uw volledige naam (optioneel)"
@@ -1299,7 +1298,7 @@ nl:
username:
title: "Gebruikersnaam"
instructions: "uniek, geen spaties, kort"
- short_instructions: "Mensen kunnen u vermelden als @%{username}"
+ short_instructions: "Mensen kunnen je vermelden als @%{username}"
available: "Uw gebruikersnaam is beschikbaar"
not_available: "Niet beschikbaar. %{suggestion} proberen?"
not_available_no_suggestion: "Niet beschikbaar"
@@ -1310,8 +1309,8 @@ nl:
required: "Voer een gebruikersnaam in"
edit: "Gebruikersnaam bewerken"
locale:
- title: "Taal van interface"
- instructions: "Taal van de gebruikersinterface. Deze verandert zodra u de pagina opnieuw laadt."
+ title: "Interfacetaal"
+ instructions: "Taal van de gebruikersinterface. Deze verandert wanneer de pagina wordt vernieuwd."
default: "(standaard)"
any: "alle"
password_confirmation:
@@ -1326,11 +1325,11 @@ nl:
not_you: "Niet u?"
show_all: "Alles tonen (%{count})"
show_few: "Minder tonen"
- was_this_you: "Was u dit?"
- was_this_you_description: "Als u dit niet was, raden we aan om uw wachtwoord te wijzigen en u overal af te melden."
+ was_this_you: "Was jij dit?"
+ was_this_you_description: "Als jij het niet was, raden we je aan om je wachtwoord te wijzigen en je overal af te melden."
browser_and_device: "%{browser} op %{device}"
secure_account: "Mijn account beveiligen"
- latest_post: "U hebt voor het laatst een bericht geplaatst…"
+ latest_post: "Je hebt als laatste geplaatst…"
device_location: '%{device} – %{location}'
browser_active: '%{browser} | nu actief'
browser_last_seen: "%{browser} | %{date}"
@@ -1358,37 +1357,37 @@ nl:
title: "Een melding sturen wanneer geliket"
always: "Altijd"
first_time_and_daily: "De eerste keer dat een bericht is geliket en dagelijks"
- first_time: "De eerste keer dat een bericht is geliket"
+ first_time: "Eerste keer dat een bericht is geliket"
never: "Nooit"
email_previous_replies:
- title: "Vorige antwoorden onder e-mails bijvoegen"
+ title: "Eerdere antwoorden bijvoegen onderaan e-mails"
unless_emailed: "tenzij eerder verzonden"
always: "altijd"
never: "nooit"
email_digests:
- title: "Als ik hier niet kom, mij een e-mailsamenvatting van populaire topics en antwoorden sturen"
+ title: "Als ik hier niet kom, stuur me dan een e-mailsamenvatting van populaire topics en antwoorden"
every_30_minutes: "elke 30 minuten"
every_hour: "elk uur"
daily: "dagelijks"
weekly: "wekelijks"
- every_month: "elke maand"
+ every_month: "maandelijks"
every_six_months: "elke zes maanden"
email_level:
always: "altijd"
only_when_away: "alleen wanneer afwezig"
never: "nooit"
- include_tl0_in_digests: "Bijdragen van nieuwe gebruikers in e-mailsamenvattingen bijvoegen"
- email_in_reply_to: "Fragment van antwoord op bericht in e-mails bijvoegen"
- other_settings: "Overige"
+ include_tl0_in_digests: "Content van nieuwe gebruikers opnemen in samenvattings-e-mails"
+ email_in_reply_to: "Fragment van antwoord op bericht opnemen in e-mails"
+ other_settings: "Overig"
categories_settings: "Categorieën"
new_topic_duration:
- label: "Topics als nieuw beschouwen wanneer"
+ label: "Topics als nieuw beschouwen als"
not_viewed: "ik ze nog niet heb bekeken"
- last_here: "ze sinds mijn laatste bezoek zijn aangemaakt"
- after_1_day: "ze de afgelopen dag zijn aangemaakt"
- after_2_days: "ze de afgelopen 2 dagen zijn aangemaakt"
- after_1_week: "ze de afgelopen week zijn aangemaakt"
- after_2_weeks: "ze de afgelopen 2 weken zijn aangemaakt"
+ last_here: "ze sinds mijn laatste bezoek zijn gemaakt"
+ after_1_day: "ze de afgelopen dag zijn gemaakt"
+ after_2_days: "ze de afgelopen 2 dagen zijn gemaakt"
+ after_1_week: "ze de afgelopen week zijn gemaakt"
+ after_2_weeks: "ze de afgelopen 2 weken zijn gemaakt"
auto_track_topics: "Automatisch topics volgen die ik heb bezocht"
auto_track_options:
never: "nooit"
@@ -1410,16 +1409,16 @@ nl:
redeemed_tab: "Verzilverd"
redeemed_tab_with_count: "Verzilverd (%{count})"
invited_via: "Uitnodiging"
- invited_via_link: "koppeling %{key} (%{count} / %{max} verzilverd)"
+ invited_via_link: "link %{key} (%{count} / %{max} verzilverd)"
groups: "Groepen"
topic: "Topic"
- sent: "Aangemaakt/Laatst Verstuurd"
+ sent: "Gemaakt/laatst verzonden"
expires_at: "Verloopt"
edit: "Bewerken"
remove: "Verwijderen"
- copy_link: "Koppeling ophalen"
+ copy_link: "Link ophalen"
reinvite: "E-mail opnieuw versturen"
- reinvited: "Uitnodiging opnieuw verstuurd"
+ reinvited: "Uitnodiging opnieuw verzonden"
removed: "Verwijderd"
search: "typ om uitnodigingen te zoeken..."
user: "Uitgenodigde gebruiker"
@@ -1429,61 +1428,61 @@ nl:
other: "De eerste %{count} uitnodigingen worden getoond."
redeemed: "Verzilverde uitnodigingen"
redeemed_at: "Verzilverd"
- pending: "Uitstaande uitnodigingen"
+ pending: "Openstaande uitnodigingen"
topics_entered: "Topics bekeken"
posts_read_count: "Berichten gelezen"
expired: "Deze uitnodiging is verlopen."
- remove_all: "Verwijder Verlopen uitnodigingen"
- removed_all: "Alle Verlopen uitnodigingen verwijderd!"
- remove_all_confirm: "Weet u zeker dat u alle verlopen uitnodigingen wilt verwijderen?"
- reinvite_all: "Alle uitnodigingen opnieuw versturen"
- reinvite_all_confirm: "Weet u zeker dat u alle uitnodigingen opnieuw wilt versturen?"
- reinvited_all: "Alle uitnodigingen zijn verstuurd!"
+ remove_all: "Verlopen uitnodigingen verwijderen"
+ removed_all: "Alle verlopen uitnodigingen verwijderd!"
+ remove_all_confirm: "Weet je zeker dat je alle verlopen uitnodigingen wilt verwijderen?"
+ reinvite_all: "Alle uitnodigingen opnieuw sturen"
+ reinvite_all_confirm: "Weet je zeker dat je alle uitnodigingen opnieuw wilt sturen?"
+ reinvited_all: "Alle uitnodigingen zijn verzonden!"
time_read: "Leestijd"
days_visited: "Dagen bezocht"
- account_age_days: "Leeftijd van account in dagen"
+ account_age_days: "Accountleeftijd in dagen"
create: "Uitnodigen"
- generate_link: "Uitnodigingskoppeling maken"
- link_generated: "Hier is uw uitnodigingskoppeling!"
- valid_for: "De uitnodigingskoppeling is alleen geldig voor dit e-mailadres: %{email}"
+ generate_link: "Uitnodigingslink maken"
+ link_generated: "Hier is je uitnodigingslink!"
+ valid_for: "De uitnodigingslink is alleen geldig voor dit e-mailadres: %{email}"
single_user: "Uitnodigen via e-mail"
- multiple_user: "Uitnodigen via koppeling"
+ multiple_user: "Uitnodigen via link"
invite_link:
- title: "Uitnodigingskoppeling"
- success: "Uitnodigingskoppeling succesvol gegenereerd!"
- error: "Er is een fout opgetreden bij het genereren van de uitnodigingskoppeling"
+ title: "Uitnodigingslink"
+ success: "Uitnodigingslink gegenereerd!"
+ error: "Er is een fout opgetreden bij het genereren van de uitnodigingslink"
invite:
new_title: "Uitnodiging maken"
edit_title: "Uitnodiging bewerken"
- instructions: "Deze koppeling delen om meteen toegang tot deze website te verlenen:"
- copy_link: "koppeling kopiëren"
- show_advanced: "Geavanceerde opties tonen"
+ instructions: "Deel deze link om direct toegang te geven tot deze site:"
+ copy_link: "link kopiëren"
+ show_advanced: "Geavanceerde opties weergeven"
hide_advanced: "Geavanceerde opties verbergen"
- email_or_domain_placeholder: "naam@example.com of example.com"
+ email_or_domain_placeholder: "naam@voorbeeld.com of voorbeeld.com"
add_to_groups: "Toevoegen aan groepen"
expires_at: "Verloopt na"
custom_message: "Optioneel persoonlijk bericht"
- send_invite_email: "E-mail opslaan en versturen"
+ send_invite_email: "E-mail opslaan en verzenden"
save_invite: "Uitnodiging opslaan"
invite_saved: "Uitnodiging opgeslagen."
bulk_invite:
none: "Geen uitnodigingen om weer te geven op deze pagina."
text: "Bulkuitnodiging"
instructions: |
- john@smith.com,eerste_groepsnaam;tweede_groepsnaam,topic_id
- jan@pietersen.com,eerste_groepsnaam;tweede_groepsnaam,topic_id
+
Weet u het zeker?"
- other: "Als u deze groep verwijdert, worden %{count} berichten verweesd, groepsleden hebben er geen toegang meer toe.
Weet u het zeker?"
+ one: "Als je deze groep verwijdert, wordt %{count} bericht verweesd en hebben groepsleden er geen toegang meer toe.
Weet je het zeker?"
+ other: "Als je deze groep verwijdert, worden %{count} berichten verweesd en hebben groepsleden er geen toegang meer toe.
Weet je het zeker?"
delete_failed: "Kan groep niet verwijderen. Als dit een automatische groep is, kan deze niet worden verwijderd."
delete_automatic_group: Dit is een automatische groep en kan niet worden verwijderd.
delete_owner_confirm: "Eigenaarsprivileges van '%{username}' verwijderen?"
@@ -3736,12 +3735,12 @@ nl:
new_key: Nieuwe API-sleutel
revoked: Ingetrokken
delete: Definitief verwijderen
- not_shown_again: Deze sleutel wordt niet meer weergegeven. Zorg ervoor dat u een kopie maakt voordat u doorgaat.
+ not_shown_again: Deze sleutel wordt niet meer weergegeven. Zorg dat je een kopie maakt voordat je verder gaat.
continue: Doorgaan
scopes:
description: |
- Bij het gebruik van scopes kunt u een API-sleutel tot een bepaalde groep eindpunten beperken.
- U kunt ook definiëren welke parameters worden toegestaan. Gebruik komma's om meerdere waarden te scheiden.
+ Bij het gebruik van bereiken kun je een API-sleutel beperken tot een bepaalde groep eindpunten.
+ Je kunt ook definiëren welke parameters worden toegestaan. Gebruik komma's om meerdere waarden te scheiden.
title: Scopes
read_only: Alleen-lezen
global: Globaal
@@ -3770,8 +3769,8 @@ nl:
web_hooks:
title: "Webhooks"
none: "Er zijn op dit moment geen webhooks."
- instruction: "Via webhooks kan Discourse externe services waarschuwen wanneer bepaalde gebeurtenissen op uw website plaatsvinden. Zodra de webhook wordt geactiveerd, wordt een POST-aanvraag naar opgegeven URL's verstuurd."
- detailed_instruction: "Zodra de gekozen gebeurtenis plaatsvindt, wordt een POST-aanvraag naar de opgegeven URL verstuurd."
+ instruction: "Via webhooks kan Discourse externe services waarschuwen wanneer bepaalde gebeurtenissen plaatsvinden op je website. Zodra de webhook wordt geactiveerd, wordt een POST-verzoek gestuurd naar opgegeven URL's."
+ detailed_instruction: "Zodra de gekozen gebeurtenis plaatsvindt, wordt een POST-aanvraag naar de opgegeven URL gestuurd."
new: "Nieuwe webhook"
create: "Aanmaken"
save: "Opslaan"
@@ -3781,11 +3780,10 @@ nl:
go_back: "Terug naar lijst"
payload_url: "Payload-URL"
payload_url_placeholder: "https://example.com/postreceive"
- warn_local_payload_url: "Het lijkt erop dat u probeert de webhook in te stellen op een lokale URL. Gebeurtenissen die op een lokaal adres worden afgeleverd, kunnen bijwerkingen of onverwacht gedrag veroorzaken. Doorgaan?"
secret_invalid: "Geheim mag geen lege tekens bevatten."
secret_too_short: "Geheim dient uit minimaal 12 tekens te bestaan."
secret_placeholder: "Een optionele tekenreeks, gebruikt voor het aanmaken van een ondertekening"
- event_type_missing: "U dient minstens één gebeurtenistype in te stellen."
+ event_type_missing: "Je moet minimaal één gebeurtenistype instellen."
content_type: "Inhoudstype"
secret: "Geheim"
event_chooser: "Welke gebeurtenissen moeten deze webhook activeren?"
@@ -3851,7 +3849,7 @@ nl:
other: "Voltooid in %{count} seconden."
request: "Aanvraag"
response: "Reactie"
- redeliver_confirm: "Weet u zeker dat u dezelfde payload opnieuw wilt afleveren?"
+ redeliver_confirm: "Weet je zeker dat je dezelfde payload opnieuw wilt afleveren?"
headers: "Koppen"
payload: "Payload"
body: "Inhoud"
@@ -3868,7 +3866,7 @@ nl:
title: "Plug-ins"
installed: "Geïnstalleerde plug-ins"
name: "Naam"
- none_installed: "U hebt geen plug-ins geïnstalleerd."
+ none_installed: "Je hebt geen plug-ins geïnstalleerd."
version: "Versie"
enabled: "Ingeschakeld?"
is_enabled: "J"
@@ -3881,18 +3879,18 @@ nl:
title: "Back-ups"
menu:
backups: "Back-ups"
- logs: "Logboeken"
+ logs: "Logs"
none: "Geen back-up beschikbaar."
read_only:
enable:
title: "Alleen-lezenmodus inschakelen"
label: "Alleen-lezen inschakelen"
- confirm: "Weet u zeker dat u de alleen-lezenmodus wilt inschakelen?"
+ confirm: "Weet je zeker dat je de alleen-lezenmodus wilt inschakelen?"
disable:
title: "Alleen-lezenmodus uitschakelen"
label: "Alleen-lezen uitschakelen"
logs:
- none: "Nog geen logboeken..."
+ none: "Nog geen logs..."
columns:
filename: "Bestandsnaam"
size: "Grootte"
@@ -3901,47 +3899,47 @@ nl:
title: "Een back-up naar deze instantie uploaden"
uploading: "Uploaden..."
uploading_progress: "Uploaden... %{progress}%"
- success: "'%{filename}' is succesvol geüpload. Het bestand wordt nu verwerkt en zal na hoogstens een minuut in de lijst verschijnen."
+ success: "'%{filename}' is geüpload. Het bestand wordt nu verwerkt, het kan tot een minuut duren voordat het in de lijst wordt weergegeven."
error: "Er is een fout opgetreden bij het uploaden van '%{filename}': %{message}"
operations:
is_running: "Er wordt al een bewerking uitgevoerd..."
- failed: "De bewerking %{operation} is mislukt. Controleer de logboeken."
+ failed: "%{operation} mislukt. Controleer de logs."
cancel:
label: "Annuleren"
title: "De huidige bewerking annuleren"
- confirm: "Weet u zeker dat u de huidige bewerking wilt annuleren?"
+ confirm: "Weet je zeker dat je de huidige bewerking wilt annuleren?"
backup:
label: "Back-up maken"
title: "Een back-up maken"
- confirm: "Wilt u een nieuwe back-up starten?"
+ confirm: "Wil je een nieuwe back-up starten?"
without_uploads: "Ja (geen uploads bijvoegen)"
download:
label: "Downloaden"
- title: "E-mail met downloadkoppeling verzenden"
- alert: "Er is een koppeling voor het downloaden van deze back-up naar u verzonden via e-mail."
+ title: "E-mail met downloadlink sturen"
+ alert: "Er is een link naar je gestuurd via e-mail om deze back-up te downloaden."
destroy:
title: "De back-up verwijderen"
- confirm: "Weet u zeker dat u deze back-up wilt verwijderen?"
+ confirm: "Weet je zeker dat je deze back-up wilt verwijderen?"
restore:
is_disabled: "Terugzetten is uitgeschakeld in de website-instellingen."
label: "Terugzetten"
title: "De back-up terugzetten"
- confirm: "Weet u zeker dat u deze back-up wilt terugzetten?"
+ confirm: "Weet je zeker dat je deze back-up wilt terugzetten?"
rollback:
label: "Terugdraaien"
title: "De database naar de vorige werkende status terugzetten"
- confirm: "Weet u zeker dat u de database naar de vorige werkende status wilt terugzetten?"
+ confirm: "Weet je zeker dat je de database wilt terugzetten naar de vorige werkende status?"
location:
local: "Lokale opslag"
s3: "S3"
backup_storage_error: "Toegang tot back-upopslag is mislukt: %{error_message}"
export_csv:
- success: "Exporteren is gestart; u ontvangt een bericht zodra het proces is voltooid."
- failed: "Exporteren is mislukt. Controleer de logboeken."
+ success: "Exporteren is gestart. Je ontvangt een bericht zodra het proces is voltooid."
+ failed: "Exporteren mislukt. Controleer de logs."
button_text: "Exporteren"
button_title:
user: "Volledige gebruikerslijst exporteren in CSV-indeling"
- staff_action: "Volledig stafactielogboek exporteren in CSV-indeling"
+ staff_action: "Volledige stafactielog exporteren in CSV-indeling"
screened_email: "Volledige lijst van gecontroleerde e-mails exporteren in CSV-indeling"
screened_ip: "Volledige lijst van gecontroleerde IP-adressen exporteren in CSV-indeling"
screened_url: "Volledige lijst van gecontroleerde URL's exporteren in CSV-indeling"
@@ -3960,7 +3958,7 @@ nl:
new_style: "Nieuwe stijl"
install: "Installeren"
delete: "Verwijderen"
- delete_confirm: 'Weet u zeker dat u ''%{theme_name}'' wilt verwijderen?'
+ delete_confirm: 'Weet je zeker dat je ''%{theme_name}'' wilt verwijderen?'
color: "Kleur"
opacity: "Ondoorzichtigheid"
copy_to_clipboard: "Kopiëren naar klembord"
@@ -3970,10 +3968,10 @@ nl:
email_templates:
title: "E-mailadres"
subject: "Onderwerp"
- multiple_subjects: "Deze e-mailsjabloon heeft meerdere onderwerpen."
+ multiple_subjects: "Deze e-mailsjabloon heeft meerdere topics."
body: "Body"
revert: "Wijzigingen ongedaan maken"
- revert_confirm: "Weet u zeker dat u uw wijzigingen ongedaan wilt maken?"
+ revert_confirm: "Weet je zeker dat je je wijzigingen ongedaan wilt maken?"
theme:
theme: "Thema"
component: "Onderdeel"
@@ -3984,7 +3982,7 @@ nl:
themes_intro_emoji: "kunstenares-emoji"
beginners_guide_title: "Beginnersgids voor het gebruik van Discourse-thema's"
developers_guide_title: "Ontwikkelaarsgids voor Discourse-thema's"
- browse_themes: "Gemeenschapsthema's bekijken"
+ browse_themes: "Communitythema's bekijken"
customize_desc: "Aanpassen:"
title: "Thema's"
create: "Aanmaken"
@@ -3992,8 +3990,8 @@ nl:
create_name: "Naam"
long_title: "Kleuren, CSS en HTML-inhoud van uw website aanpassen"
edit: "Bewerken"
- edit_confirm: "Dit is een extern thema; als u CSS/HTML bewerkt, worden uw wijzigingen de volgende keer dat u het thema bijwerkt gewist."
- update_confirm: "Deze lokale wijzigingen worden door de update gewist. Weet u zeker dat u wilt doorgaan?"
+ edit_confirm: "Dit is een extern thema. Als je CSS/HTML bewerkt, worden je wijzigingen gewist de volgende keer dat je het thema bijwerkt."
+ update_confirm: "Deze lokale wijzigingen worden gewist door de update. Weet je zeker dat je wilt doorgaan?"
update_confirm_yes: "Ja, doorgaan met de update"
common: "Algemeen"
desktop: "Desktop"
@@ -4019,11 +4017,11 @@ nl:
theme_components: "Themaonderdelen"
add_all_themes: "Alle thema's toevoegen"
convert: "Converteren"
- convert_component_alert: "Weet u zeker dat u dit onderdeel naar een thema wilt converteren? Het wordt als onderdeel van %{relatives} verwijderd."
+ convert_component_alert: "Weet je zeker dat je dit onderdeel naar een thema wilt converteren? Het wordt verwijderd als onderdeel van %{relatives}."
convert_component_tooltip: "Dit onderdeel naar een thema converteren"
- convert_component_alert_generic: "Weet u zeker dat u dit onderdeel naar een thema wilt converteren?"
- convert_theme_alert: "Weet u zeker dat u dit thema naar een onderdeel wilt converteren? Het wordt als bovenliggend thema van %{relatives} verwijderd."
- convert_theme_alert_generic: "Weet u zeker dat u dit thema naar een onderdeel wilt converteren?"
+ convert_component_alert_generic: "Weet je zeker dat je dit onderdeel naar een thema wilt converteren?"
+ convert_theme_alert: "Weet je zeker dat je dit thema naar een onderdeel wilt converteren? Het wordt verwijderd als bovenliggend thema van %{relatives}."
+ convert_theme_alert_generic: "Weet je zeker dat je dit thema naar een onderdeel wilt converteren?"
convert_theme_tooltip: "Dit thema naar een onderdeel converteren"
inactive_themes: "Inactieve thema's:"
inactive_components: "Ongebruikte onderdelen:"
@@ -4034,7 +4032,7 @@ nl:
and_x_more: "en nog %{count}."
collapse: Samenvouwen
uploads: "Uploads"
- no_uploads: "U kunt aan uw thema gerelateerde assets uploaden, zoals lettertypen en afbeeldingen"
+ no_uploads: "Je kunt assets behorende bij je thema uploaden, zoals lettertypen en afbeeldingen"
add_upload: "Upload toevoegen"
upload_file_tip: "Kies een asset om te uploaden (png, woff2, etc...)"
variable_name: "SCSS-variabelenaam:"
@@ -4045,32 +4043,31 @@ nl:
must_be_unique: "Ongeldige naam van variabele. Moet uniek zijn."
upload: "Upload"
select_component: "Selecteer een onderdeel..."
- unsaved_changes_alert: "U hebt uw wijzigingen nog niet opgeslagen. Wilt u ze negeren en verdergaan?"
- unsaved_parent_themes: "U hebt het onderdeel niet aan thema's toegewezen. Wilt u verdergaan?"
+ unsaved_changes_alert: "Je hebt je wijzigingen nog niet opgeslagen. Wil je ze negeren en verder gaan?"
+ unsaved_parent_themes: "Je hebt het onderdeel niet toegewezen aan thema's. Wil je verder gaan?"
discard: "Negeren"
stay: "Blijven"
css_html: "Aangepaste CSS/HTML"
edit_css_html: "CSS/HTML bewerken"
- edit_css_html_help: "U hebt geen CSS of HTML bewerkt"
- delete_upload_confirm: "Deze upload verwijderen? (Thema-CSS zou kunnen stoppen met werken!)"
+ edit_css_html_help: "Je hebt geen CSS of HTML bewerkt"
+ delete_upload_confirm: "Deze upload verwijderen? (Thema-CSS kan stoppen met werken!)"
component_on_themes: "Onderdeel voor deze thema's bijvoegen"
included_components: "Opgenomen onderdelen"
add_all: "Alle toevoegen"
import_web_tip: "Repository die thema bevat"
- direct_install_tip: "Weet u zeker dat u %{name} uit de onderstaande repository wilt installeren?"
+ direct_install_tip: "Weet je zeker dat je %{name} uit de onderstaande repository wilt installeren?"
import_web_advanced: "Geavanceerd..."
import_file_tip: ".tar.gz-, .zip- of .dcstyle.json-bestand dat thema bevat"
is_private: "Thema bevindt zich in een privé-git-repository"
remote_branch: "Branchnaam (optioneel)"
public_key: "De volgende openbare sleutel toegang tot de repo verlenen:"
- public_key_note: "Na het hierboven invoeren van een geldig privé repository-URL wordt een SSH-sleutel gegenereerd en hier weergegeven."
install: "Installeren"
installed: "Geïnstalleerd"
install_popular: "Populair"
install_upload: "Vanaf uw apparaat"
install_git_repo: "Vanaf een git-repository"
install_create: "Nieuwe maken"
- duplicate_remote_theme: "Het themaonderdeel “%{name}” is al geïnstalleerd, weet u zeker dat u nog een kopie wilt installeren?"
+ duplicate_remote_theme: "Het themaonderdeel “%{name}” is al geïnstalleerd, weet je zeker dat je nog een exemplaar wilt installeren?"
about_theme: "Over"
license: "Licentie"
version: "Versie:"
@@ -4100,7 +4097,7 @@ nl:
one: "Thema loopt %{count} commit achter!"
other: "Thema loopt %{count} commits achter!"
compare_commits: "(Nieuwe commits bekijken)"
- remote_theme_edits: "Als u dit thema wilt bewerken, moet u een wijziging indienen in de repository"
+ remote_theme_edits: "Als je dit thema wilt bewerken, moet je een wijziging indienen in de repository"
repo_unreachable: "Kon geen contact krijgen met de Git-repository van dit thema. Foutbericht:"
imported_from_archive: "Dit thema is vanuit een .zip-bestand geïmporteerd"
scss:
@@ -4164,10 +4161,10 @@ nl:
description: "De hoofdachtergrondkleur, en tekstkleur van sommige knoppen."
tertiary:
name: "tertiaire"
- description: "Koppelingen, sommige knoppen, meldingen en accentkleur."
+ description: "Links, sommige knoppen, meldingen en accentkleur."
quaternary:
name: "quaternaire"
- description: "Navigatiekoppelingen."
+ description: "Navigatielinks."
header_background:
name: "koptekstachtergrond"
description: "Achtergrondkleur van de koptekst van de website."
@@ -4202,7 +4199,7 @@ nl:
html: "HTML-sjabloon"
css: "CSS"
reset: "Standaardwaarden terugzetten"
- reset_confirm: "Weet u zeker dat u de standaardwaarden van %{fieldName} wilt terugzetten en alle wijzigingen wilt verwerpen?"
+ reset_confirm: "Weet je zeker dat je de standaardwaarde voor %{fieldName} wilt herstellen en alle wijzigingen wilt verwerpen?"
save_error_with_reason: "Uw wijzigingen zijn niet opgeslagen. %{error}"
instructions: "De sjabloon waarin alle html-e-mails worden gerenderd aanpassen, en stileren via CSS."
email:
@@ -4212,12 +4209,12 @@ nl:
preview_digest: "Voorbeeld van samenvatting"
advanced_test:
title: "Geavanceerde test"
- desc: "Bekijken hoe Discourse ontvangen e-mails verwerkt. Plak hieronder het volledige oorspronkelijke e-mailbericht om de e-mail goed te kunnen laten verwerken."
+ desc: "Zie hoe Discourse ontvangen e-mails verwerkt. Plak hieronder het volledige oorspronkelijke e-mailbericht zodat de e-mail goed wordt verwerkt."
email: "Oorspronkelijke bericht"
run: "Test uitvoeren"
text: "Hoofdtekst van geselecteerde tekst"
elided: "Weggelaten tekst"
- sending_test: "Testmail wordt verstuurd..."
+ sending_test: "Test-e-mail wordt verzonden..."
error: "FOUT - %{server_error}"
test_error: "Er is een probleem opgetreden bij het versturen van de testmail. Controleer uw mailinstellingen, controleer of uw host geen mailverbindingen blokkeert, en probeer het daarna opnieuw."
sent: "Verzonden"
@@ -4234,11 +4231,11 @@ nl:
send_test: "Testmail verzenden"
sent_test: "verzonden!"
delivery_method: "Verzendmethode"
- preview_digest_desc: "Een voorbeeld bekijken van de e-mailsamenvattingen die naar inactieve leden worden verzonden."
+ preview_digest_desc: "Bekijk een voorbeeld van de e-mailsamenvattingen die naar inactieve leden worden gestuurd."
refresh: "Vernieuwen"
send_digest_label: "Dit resultaat verzenden naar:"
send_digest: "Verzenden"
- sending_email: "E-mail wordt verzonden..."
+ sending_email: "E-mail verzenden..."
format: "Indeling"
html: "html"
text: "tekst"
@@ -4257,23 +4254,23 @@ nl:
modal:
title: "Details van inkomende e-mail"
error: "Fout"
- headers: "Kopregels"
+ headers: "Headers"
subject: "Onderwerp"
body: "Tekst"
- rejection_message: "Weigeringsmail"
+ rejection_message: "Afwijzings-e-mail"
filters:
- from_placeholder: "van@example.com"
- to_placeholder: "aan@example.com"
- cc_placeholder: "cc@example.com"
+ from_placeholder: "van@voorbeeld.com"
+ to_placeholder: "aan@voorbeeld.com"
+ cc_placeholder: "cc@voorbeeld.com"
subject_placeholder: "Onderwerp..."
error_placeholder: "Fout"
logs:
- none: "Geen logboeken gevonden."
+ none: "Geen logs gevonden."
filters:
title: "Filter"
user_placeholder: "gebruikersnaam"
- address_placeholder: "naam@example.com"
- type_placeholder: "samenvatting, registratie..."
+ address_placeholder: "naam@voorbeeld.com"
+ type_placeholder: "digest, registratie..."
reply_key_placeholder: "antwoordsleutel"
moderation_history:
performed_by: "Uitgevoerd door"
@@ -4286,12 +4283,12 @@ nl:
delete_topic: "Topic verwijderd"
post_approved: "Bericht goedgekeurd"
logs:
- title: "Logboeken"
+ title: "Logs"
action: "Actie"
created_at: "Gemaakt"
- last_match_at: "Laatste overeenkomst"
- match_count: "Overeenkomsten"
- ip_address: "IP"
+ last_match_at: "Laatst gematcht"
+ match_count: "Matches"
+ ip_address: "IP-adres"
topic_id: "Topic-ID"
post_id: "Bericht-ID"
category_id: "Categorie-ID"
@@ -4361,14 +4358,14 @@ nl:
deactivate_user: "gebruiker deactiveren"
change_readonly_mode: "alleen-lezenmodus wijzigen"
backup_download: "back-up downloaden"
- backup_destroy: "back-up verwijderen"
+ backup_destroy: "back-up vernietigen"
reviewed_post: "bericht beoordeeld"
custom_staff: "aangepaste actie voor plug-in"
post_locked: "bericht vergrendeld"
post_edit: "bericht bewerken"
post_unlocked: "bericht ontgrendeld"
check_personal_message: "persoonlijk bericht controleren"
- disabled_second_factor: "tweefactorauthenticatie uitschakelen"
+ disabled_second_factor: "tweeledige verificatie uitschakelen"
topic_published: "topic gepubliceerd"
post_approved: "bericht goedgekeurd"
post_rejected: "bericht afgekeurd"
@@ -4384,20 +4381,20 @@ nl:
web_hook_update: "webhook bijwerken"
web_hook_destroy: "webhook verwijderen"
web_hook_deactivate: "webhook deactiveren"
- embeddable_host_create: "inbedbare host maken"
- embeddable_host_update: "inbedbare host bijwerken"
- embeddable_host_destroy: "inbedbare host verwijderen"
+ embeddable_host_create: "insluitbare host maken"
+ embeddable_host_update: "insluitbare host bijwerken"
+ embeddable_host_destroy: "insluitbare host verwijderen"
change_theme_setting: "thema-instelling wijzigen"
disable_theme_component: "themaonderdeel uitschakelen"
enable_theme_component: "themaonderdeel inschakelen"
revoke_title: "titel intrekken"
change_title: "titel wijzigen"
- api_key_create: "api-sleutel maken"
- api_key_update: "api-sleutel bijwerken"
- api_key_destroy: "api-sleutel verwijderen"
+ api_key_create: "API-sleutel maken"
+ api_key_update: "API-sleutel bijwerken"
+ api_key_destroy: "API-sleutel verwijderen"
override_upload_secure_status: "status beveiligd uploaden negeren"
page_published: "pagina gepubliceerd"
- page_unpublished: "pagina niet gepubliceerd"
+ page_unpublished: "pagina gedepubliceerd"
add_email: "e-mailadres toevoegen"
update_email: "e-mailadres bijwerken"
destroy_email: "e-mailadres verwijderen"
@@ -4421,8 +4418,8 @@ nl:
domain: "Domein"
screened_ips:
title: "Gecontroleerde IP-adressen"
- description: 'IP-adressen die in de gaten worden gehouden. Gebruik ''Toestaan'' om IP-adressen op de acceptatielijst te zetten.'
- delete_confirm: "Weet u zeker dat u de regel voor %{ip_address} wilt verwijderen?"
+ description: 'IP-adressen die worden geobserveerd. Gebruik ''Toestaan'' om IP-adressen op de acceptatielijst te zetten.'
+ delete_confirm: "Weet je zeker dat je de regel voor %{ip_address} wilt verwijderen?"
actions:
block: "Blokkeren"
do_nothing: "Toestaan"
@@ -4436,7 +4433,7 @@ nl:
text: "Samenvoegen"
title: "Maakt nieuwe subnet-banvermeldingen als er minstens 'min_ban_entries_for_roll_up'-vermeldingen zijn."
search_logs:
- title: "Zoeklogboeken"
+ title: "Zoeken in logs"
term: "Term"
searches: "Zoekopdrachten"
click_through_rate: "CTR"
@@ -4447,38 +4444,38 @@ nl:
click_through_only: "Alle (alleen doorklikken)"
header_search_results: "Zoekresultaten voor koptekst"
logster:
- title: "Foutlogboeken"
+ title: "Foutenlogs"
watched_words:
- title: "In de gaten gehouden woorden"
+ title: "Geobserveerde woorden"
search: "zoeken"
clear_filter: "Wissen"
show_words:
- one: "%{count} woord tonen"
- other: "%{count} woorden tonen"
+ one: "%{count} woord weergeven"
+ other: "%{count} woorden weergeven"
download: Downloaden
clear_all: Alles wissen
- clear_all_confirm: "Weet u zeker dat u alle in de gaten gehouden woorden wilt verwijderen voor de %{action} actie?"
+ clear_all_confirm: "Weet je zeker dat je alle geobserveerde woorden wilt verwijderen voor de actie %{action}?"
actions:
block: "Blokkeren"
censor: "Censureren"
- require_approval: "Heeft goedkeuring nodig"
+ require_approval: "Goedkeuring vereisen"
flag: "Markeren"
replace: "Vervangen"
tag: "Taggen"
silence: "Dempen"
- link: "Koppeling"
+ link: "Link"
action_descriptions:
block: "Het plaatsen van berichten die deze woorden bevatten tegengaan. Gebruikers zien een foutbericht wanneer ze hun bericht proberen te verzenden."
censor: "Berichten met deze woorden toestaan, maar deze vervangen door tekens die de gecensureerde woorden verbergen."
require_approval: "Berichten die deze woorden bevatten, vereisen goedkeuring door stafleden voordat ze kunnen worden bekeken."
- flag: "Berichten met deze woorden toestaan, maar deze markeren als ongepast zodat moderators ze kunnen beoordelen."
- tag: "Automatisch topics taggen op basis van het eerste bericht"
+ flag: "Sta berichten met deze woorden toe, maar markeer ze als ongepast, zodat moderators ze kunnen beoordelen."
+ tag: "Tag topics automatisch op basis van het eerste bericht"
form:
placeholder_regexp: "reguliere expressie"
replace_label: "Vervanging"
tag_label: "Tag"
- link_label: "Koppeling"
- link_placeholder: "https://example.com"
+ link_label: "Link"
+ link_placeholder: "https://voorbeeld.com"
add: "Toevoegen"
success: "Succes"
exists: "Bestaat al"
@@ -4486,22 +4483,22 @@ nl:
upload_successful: "Uploaden geslaagd. Woorden zijn toegevoegd."
test:
button_label: "Testen"
- description: "Voer hieronder tekst in voor controle op in de gaten gehouden woorden"
+ description: "Voer hieronder tekst in om te controleren op matches met geobserveerde woorden"
found_matches: "Gevonden overeenkomsten:"
no_matches: "Geen overeenkomsten gevonden"
impersonate:
- title: "Aanmelden als gebruiker"
- help: "Gebruik dit hulpmiddel om een gebruikersaccount voor debugdoeleinden te imiteren. U moet zich afmelden als u klaar bent."
- not_found: "Die gebruiker kan niet worden gevonden."
- invalid: "Sorry, u mag zich niet aanmelden als die gebruiker."
+ title: "Imiteren"
+ help: "Gebruik deze tool om een gebruikersaccount te imiteren voor debugdoeleinden. Je moet je afmelden wanneer je klaar bent."
+ not_found: "Die gebruiker kon niet worden gevonden."
+ invalid: "Sorry, je mag je niet aanmelden als die gebruiker."
users:
title: "Gebruikers"
create: "Beheerder toevoegen"
- last_emailed: "Laatst gemaild"
+ last_emailed: "Laatst ge-e-maild"
not_found: "Sorry, die gebruikersnaam bestaat niet in ons systeem."
id_not_found: "Sorry, die gebruikers-ID bestaat niet in ons systeem."
active: "Geactiveerd"
- show_emails: "E-mailadressen tonen"
+ show_emails: "E-mailadressen weergeven"
hide_emails: "E-mailadressen verbergen"
nav:
new: "Nieuw"
@@ -4509,26 +4506,26 @@ nl:
staff: "Stafleden"
suspended: "Geschorst"
silenced: "Gedempt"
- staged: "Staged"
+ staged: "Gefaseerd"
approved: "Goedgekeurd?"
titles:
active: "Actieve gebruikers"
new: "Nieuwe gebruikers"
- pending: "Nog niet goedgekeurde gebruikers"
- newuser: "Gebruikers op vertrouwensniveau 0 (Nieuwe gebruiker)"
- basic: "Gebruikers op vertrouwensniveau 1 (Basisgebruiker)"
- member: "Gebruikers op vertrouwensniveau 2 (Lid)"
- regular: "Gebruikers op vertrouwensniveau 3 (Vaste gebruiker)"
- leader: "Gebruikers op vertrouwensniveau 4 (Leider)"
+ pending: "Gebruikers in afwachting van beoordeling"
+ newuser: "Gebruikers op vertrouwensniveau 0 (nieuwe gebruiker)"
+ basic: "Gebruikers op vertrouwensniveau 1 (basisgebruiker)"
+ member: "Gebruikers op vertrouwensniveau 2 (lid)"
+ regular: "Gebruikers op vertrouwensniveau 3 (regelmatige gebruiker)"
+ leader: "Gebruikers op vertrouwensniveau 4 (leider)"
staff: "Stafleden"
admins: "Beheerders"
moderators: "Moderators"
silenced: "Gedempte gebruikers"
suspended: "Geschorste gebruikers"
- staged: "Staged gebruikers"
+ staged: "Gefaseerde gebruikers"
not_verified: "Niet geverifieerd"
check_email:
- title: "E-mailadres van deze gebruiker tonen"
+ title: "E-mailadres van deze gebruiker weergeven"
text: "Tonen"
check_sso:
title: "SSO-payload tonen"
@@ -4537,24 +4534,24 @@ nl:
suspend_failed: "Er is iets misgegaan bij het schorsen van deze gebruiker: %{error}"
unsuspend_failed: "Er is iets misgegaan bij het opheffen van de schorsing van deze gebruiker: %{error}"
suspend_duration: "Hoelang wordt de gebruiker geschorst?"
- suspend_reason_label: "Waarom schorst u deze gebruiker? Deze tekst zal voor iedereen zichtbaar zijn op de profielpagina van deze gebruiker, en zal aan de gebruiker worden getoond als deze zich probeert aan te melden. Houd het kort."
- suspend_reason_hidden_label: "Waarom schorst u? Deze tekst wordt aan de gebruiker getoond wanneer deze zich probeert aan te melden. Hou het kort."
+ suspend_reason_label: "Waarom schors je deze gebruiker? Deze tekst is zichtbaar voor iedereen op de profielpagina van deze gebruiker en wordt getoond aan de gebruiker wanneer deze zich probeert aan te melden. Houd het kort."
+ suspend_reason_hidden_label: "Waarom schors je? Deze tekst wordt aan de gebruiker getoond wanneer deze zich probeert aan te melden. Houd het kort."
suspend_reason: "Reden"
suspend_reason_title: "Reden van schorsing"
suspend_reasons:
not_listening_to_staff: "Wilde niet naar feedback van stafleden luisteren"
consuming_staff_time: "Heeft onevenredig veel tijd van stafleden verbruikt"
in_wrong_place: "Op de verkeerde plek"
- no_constructive_purpose: "Geen constructief doel voor hun acties, anders dan het creëren van onenigheid binnen de gemeenschap"
+ no_constructive_purpose: "Geen constructief doel voor hun acties, anders dan het creëren van onenigheid binnen de community"
custom: "Aangepast..."
suspend_message: "E-mailbericht"
- suspend_message_placeholder: "Optioneel kunt u meer informatie over de schorsing geven, die naar de gebruiker wordt gemaild."
+ suspend_message_placeholder: "Optioneel kun je meer informatie over de schorsing geven, wat naar de gebruiker wordt gestuurd via e-mail."
suspended_by: "Geschorst door"
silence_reason: "Reden"
silenced_by: "Gedempt door"
silence_modal_title: "Gebruiker dempen"
silence_duration: "Hoelang wordt de gebruiker gedempt?"
- silence_reason_label: "Waarom dempt u deze gebruiker?"
+ silence_reason_label: "Waarom demp je deze gebruiker?"
silence_reason_placeholder: "Reden van dempen"
silence_message: "E-mailbericht"
silence_message_placeholder: "(laat leeg om standaardbericht te sturen)"
@@ -4562,8 +4559,8 @@ nl:
cant_suspend: "Deze gebruiker kan niet worden geschorst."
delete_posts_failed: "Er is een probleem opgetreden bij het verwijderen van de berichten."
post_edits: "Berichtbewerkingen"
- view_edits: "Bewerkingen bekijken"
- penalty_post_actions: "Wat wilt u met het gekoppelde bericht doen?"
+ view_edits: "Bewerkingen weergeven"
+ penalty_post_actions: "Wat wil je doen met het bijbehorende bericht?"
penalty_post_delete: "Het bericht verwijderen"
penalty_post_delete_replies: "Het bericht + alle antwoorden verwijderen"
penalty_post_edit: "Het bericht bewerken"
@@ -4572,7 +4569,7 @@ nl:
clear_penalty_history:
title: "Minpuntengeschiedenis wissen"
description: "gebruikers met minpunten kunnen geen TL3 bereiken"
- delete_all_posts_confirm_MF: "U staat op het punt {POSTS, plural, one {# bericht} other {# berichten}} en {TOPICS, plural, one {# topic} other {# topics}} te verwijderen. Weet u het zeker?"
+ delete_all_posts_confirm_MF: "Je staat op het punt {POSTS, plural, one {# bericht} other {# berichten}} en {TOPICS, plural, one {# topic} other {# topics}} te verwijderen. Weet je het zeker?"
silence: "Dempen"
unsilence: "Dempen opheffen"
silenced: "Gedempt?"
@@ -4581,31 +4578,31 @@ nl:
suspended: "Geschorst?"
staged: "Staged?"
show_admin_profile: "Beheerder"
- show_public_profile: "Openbaar profiel tonen"
- impersonate: "Aanmelden als gebruiker"
- action_logs: "Actielogboeken"
+ show_public_profile: "Openbaar profiel weergeven"
+ impersonate: "Imiteren"
+ action_logs: "Actielogs"
ip_lookup: "IP-adres zoeken"
log_out: "Afmelden"
- logged_out: "Gebruiker is op alle apparaten afgemeld"
+ logged_out: "Gebruiker is afgemeld van alle apparaten"
revoke_admin: "Beheerdersrechten intrekken"
grant_admin: "Beheerdersrechten toekennen"
- grant_admin_confirm: "We hebben u een e-mail gestuurd om de nieuwe beheerder te verifiëren. Open deze en volg de instructies."
+ grant_admin_confirm: "We hebben je een e-mail gestuurd om de nieuwe beheerder te verifiëren. Open deze en volg de instructies."
revoke_moderation: "Moderatierechten intrekken"
grant_moderation: "Moderatierechten toekennen"
unsuspend: "Schorsing opheffen"
suspend: "Schorsen"
- show_flags_received: "Ontvangen markeringen tonen"
+ show_flags_received: "Ontvangen markeringen weergeven"
flags_received_by: "Ontvangen markeren van %{username}"
flags_received_none: "Deze gebruiker heeft geen markeringen ontvangen."
reputation: Reputatie
permissions: Toestemmingen
activity: Activiteit
like_count: Gegeven / ontvangen likes
- last_100_days: "in de laatste 100 dagen"
+ last_100_days: "de afgelopen 100 dagen"
private_topics_count: Privétopics
posts_read_count: Gelezen berichten
post_count: Gemaakte berichten
- second_factor_enabled: Tweefactorauthenticatie ingeschakeld
+ second_factor_enabled: Tweeledige verificatie ingeschakeld
topics_entered: Bekeken topics
flags_given_count: Gegeven markeringen
flags_received_count: Ontvangen markeringen
@@ -4618,27 +4615,27 @@ nl:
time_read: "Leestijd"
post_edits_count: "Berichtbewerkingen"
anonymize: "Gebruiker anonimiseren"
- anonymize_confirm: "Weet u ZEKER dat u deze account wilt anonimiseren? Hierdoor worden de gebruikersnaam en het e-mailadres gewijzigd, en alle profielgegevens opnieuw ingesteld."
- anonymize_yes: "Ja, deze account anonimiseren"
- anonymize_failed: "Er is een probleem opgetreden bij het anonimiseren van de account."
+ anonymize_confirm: "Weet je ZEKER dat je dit account wilt anonimiseren? Hierdoor worden de gebruikersnaam en het e-mailadres gewijzigd en worden alle profielgegevens opnieuw ingesteld."
+ anonymize_yes: "Ja, dit account anonimiseren"
+ anonymize_failed: "Er is een probleem opgetreden bij het anonimiseren van het account."
delete: "Gebruiker verwijderen"
delete_posts:
button: "Alle berichten verwijderen"
progress:
- title: "Voortgang van het verwijderen van berichten"
+ title: "Voortgang van berichten verwijderen"
description: "Berichten verwijderen..."
confirmation:
cancel: "Annuleren"
merge:
button: "Samenvoegen"
prompt:
- title: "Overdragen & @%{username} verwijderen"
+ title: "Overdragen en @%{username}en verwijderen"
description: |
Poszukaj %{icon} aby zdecydować, o których konkretnych tematach, kategoriach i tagach chcesz otrzymywać powiadomienia. Więcej informacji znajdziesz w preferencjach powiadomień.
+ no_other_notifications_body: >
+ W tym panelu będziesz otrzymywać powiadomienia o innych rodzajach aktywności, które mogą być dla Ciebie istotne - na przykład gdy ktoś zamieści link do jednego z Twoich postów lub go edytuje.
no_notifications_page_title: "Nie masz jeszcze żadnych powiadomień"
no_notifications_page_body: >
Zostaniesz powiadomiony o działaniach bezpośrednio związanych z Tobą, w tym o odpowiedziach na Twoje tematy i posty, gdy ktoś @oznaczy Ciebie lub cytuje Ciebie, i odpowiada na tematy, które oglądasz. Powiadomienia będą również wysyłane na Twój adres e-mail, gdy nie zalogujesz się przez jakiś czas.
Poszukaj %{icon} , aby zdecydować, o których konkretnych tematach, kategoriach i tagach chcesz być powiadamiany. Aby dowiedzieć się więcej, zobacz swoje preferencje powiadomień.
@@ -1192,6 +1196,7 @@ pl_PL:
not_first_time: "To nie twój pierwszy raz?"
skip_link: "Pomiń te wskazówki"
read_later: "Przeczytam to później."
+ reset_seen_popups: "Pokaż ponownie wskazówki wprowadzające"
theme_default_on_all_devices: "Ustaw to jako domyślny motyw na wszystkich urządzeniach"
color_scheme_default_on_all_devices: "Ustaw domyślne schematy kolorów na wszystkich moich urządzeniach"
color_scheme: "Schemat kolorów"
@@ -1218,8 +1223,6 @@ pl_PL:
tags_section: "Sekcja tagów"
tags_section_instruction: "Wybrane tagi będą wyświetlane w sekcji tagów paska bocznego."
navigation_section: "Nawigacja"
- list_destination_default: "Domyślny"
- list_destination_unread_new: "Nowe/Nieprzeczytane"
change: "zmień"
featured_topic: "Wyróżniony temat"
moderator: "%{user} jest moderatorem"
@@ -1805,8 +1808,13 @@ pl_PL:
remove_status: "Usuń status"
popup:
primary: "Zrozumiano!"
+ secondary: "nie pokazuj mi tych wskazówek"
first_notification:
title: "Twoje pierwsze powiadomienie!"
+ content: "Powiadomienia służą do informowania na bieżąco o tym, co dzieje się w społeczności."
+ topic_timeline:
+ title: "Oś czasu tematu"
+ content: "Przewiń szybko post, korzystając z osi czasu tematu."
loading: "Wczytuję…"
errors:
prev_page: "podczas próby wczytania"
@@ -1839,6 +1847,8 @@ pl_PL:
enabled: "Strona jest w trybie tylko-do-odczytu. Możesz ją nadal przeglądać, ale operacje takie jak publikowanie postów, polubianie i inne są wyłączone."
login_disabled: "Logowanie jest zablokowane, gdy strona jest w trybie tylko do odczytu."
logout_disabled: "Wylogowanie jest zablokowane gdy strona jest w trybie tylko do odczytu."
+ staff_writes_only_mode:
+ enabled: "Ta strona jest w trybie tylko dla personelu. Kontynuuj przeglądanie, ale odpowiadanie, polubienia i inne działania są ograniczone tylko do członków personelu."
too_few_topics_and_posts_notice_MF: >-
Zacznijmy dyskutować! Mamy {currentTopics, plural, one {# temat} few {# tematy} other {# tematów}} i {currentPosts, plural, one {# wpis} few {# wpisy} other {# wpisów}}. Odwiedzający potrzebują więcej do dyskusji – zalecamy przynajmniej {requiredTopics, plural, one {# temat} few {# tematy} other {# tematów}} i {requiredPosts, plural, one {# wpis} few {# wpisy} other {# wpisów}}. Tylko członkowie zespołu widzą tę wiadomość.
too_few_topics_notice_MF: >-
@@ -1872,6 +1882,7 @@ pl_PL:
hide_forever: "nie, dziękuję"
hidden_for_session: "OK, zapytamy Cię jutro. Zawsze możesz też użyć opcji „Zaloguj się”, aby utworzyć konto."
intro: "Hej! Wygląda na to, że zainteresowała Cię ta dyskusja, ale nie posiadasz jeszcze konta."
+ value_prop: "Masz dość przewijania tych samych postów? Kiedy stworzysz konto, zawsze wrócisz do miejsca, w którym przerwałeś. Z kontem możesz również otrzymywać powiadomienia o nowych odpowiedziach, zapisywać zakładki i używać polubień, aby podziękować innym. Wszyscy możemy współpracować, aby ta społeczność była świetna. :heart:"
summary:
enabled_description: "Przeglądasz podsumowanie tego tematu: widoczne są jedynie najbardziej wartościowe wpisy zdaniem uczestników. "
description:
@@ -2554,17 +2565,54 @@ pl_PL:
view_all: "zobacz wszystkie %{tab}"
user_menu:
generic_no_items: "Na tej liście nie ma żadnych pozycji."
+ sr_menu_tabs: "Zakładki menu użytkownika"
view_all_notifications: "zobacz wszystkie powiadomienia"
view_all_bookmarks: "zobacz wszystkie zakładki"
view_all_messages: "wyświetl wszystkie wiadomości osobiste"
tabs:
all_notifications: "Wszystkie powiadomienia"
replies: "Odpowiedzi"
+ replies_with_unread:
+ one: "Odpowiedzi - %{count} nieprzeczytana odpowiedź"
+ few: "Odpowiedzi - %{count} nieprzeczytane odpowiedzi"
+ many: "Odpowiedzi - %{count} nieprzeczytanych odpowiedzi"
+ other: "Odpowiedzi - %{count} nieprzeczytanych odpowiedzi"
mentions: "Wzmianki"
+ mentions_with_unread:
+ one: "Wzmianki - %{count} nieprzeczytana wzmianka"
+ few: "Wzmianki - %{count} nieprzeczytane wzmianki"
+ many: "Wzmianki - %{count} nieprzeczytanych wzmianek"
+ other: "Wzmianki - %{count} nieprzeczytanych wzmianek"
likes: "Otrzymane polubienia"
+ likes_with_unread:
+ one: "Polubienia - %{count} nieprzeczytane polubienie"
+ few: "Polubienia - %{count} nieprzeczytane polubienia"
+ many: "Polubienia - %{count} nieprzeczytanych polubień"
+ other: "Polubienia - %{count} nieprzeczytanych polubień"
watching: "Obserwowane tematy"
+ watching_with_unread:
+ one: "Obserwowane tematy - %{count} nieprzeczytany obserwowany temat"
+ few: "Obserwowane tematy - %{count} nieprzeczytane obserwowane tematy"
+ many: "Obserwowane tematy - %{count} nieprzeczytanych obserwowanych tematów"
+ other: "Obserwowane tematy - %{count} nieprzeczytanych obserwowanych tematów"
messages: "Wiadomości osobiste"
+ messages_with_unread:
+ one: "Wiadomości osobiste - %{count} nieprzeczytana wiadomość"
+ few: "Wiadomości osobiste - %{count} nieprzeczytane wiadomości"
+ many: "Wiadomości osobiste - %{count} nieprzeczytanych wiadomości"
+ other: "Wiadomości osobiste - %{count} nieprzeczytanych wiadomości"
bookmarks: "Zakładki"
+ bookmarks_with_unread:
+ one: "Zakładki - %{count} nieprzeczytana zakładka"
+ few: "Zakładki - %{count} nieprzeczytane zakładki"
+ many: "Zakładki - %{count} nieprzeczytanych zakładek"
+ other: "Zakładki - %{count} nieprzeczytanych zakładek"
+ review_queue: "Kolejka do przeglądu"
+ review_queue_with_unread:
+ one: "Kolejka do przeglądu - %{count} pozycja wymaga przeglądu"
+ few: "Kolejka do przeglądu - %{count} pozycje wymagają przeglądu"
+ many: "Kolejka recenzji - %{count} pozycji wymaga przeglądu"
+ other: "Kolejka recenzji - %{count} pozycji wymaga przeglądu"
other_notifications: "Inne powiadomienia"
other_notifications_with_unread:
one: "Inne powiadomienia - %{count} nieprzeczytane powiadomienie"
@@ -2573,6 +2621,7 @@ pl_PL:
other: "Inne powiadomienia - %{count} nieprzeczytanych powiadomień"
profile: "Profil"
reviewable:
+ view_all: "zobacz wszystkie elementy do przeglądu"
queue: "Kolejka"
deleted_user: "(usunięty użytkownik)"
deleted_post: "(usunięty post)"
@@ -3145,6 +3194,7 @@ pl_PL:
few: "pokaż %{count} ukryte odpowiedzi"
many: "pokaż %{count} ukrytych odpowiedzi"
other: "pokaż %{count} ukrytych odpowiedzi"
+ sr_reply_to: "Odpowiedz na post #%{post_number} od @%{username}"
notice:
new_user: "%{user} opublikował(a) coś po raz pierwszy - powitajmy tę osobę w naszej społeczności!"
returning_user: "Minęło trochę czasu, odkąd widzieliśmy %{user} - ostatni wpis tego użytkownika był %{time}."
@@ -3392,6 +3442,7 @@ pl_PL:
all: "Wszystkie kategorie"
choose: "kategoria…"
edit: "Edytuj"
+ edit_title: "Edytuj tę kategorię"
edit_dialog_title: "Edytuj: %{categoryName}"
view: "Pokaż Tematy w Kategorii"
back: "Powrót do kategorii"
@@ -4151,12 +4202,20 @@ pl_PL:
unread_with_count: "Nieprzeczytane (%{count})"
archive: "Archiwum"
tags:
+ links:
+ add_tags:
+ content: "Dodaj tagi"
+ title: "Nie dodałeś żadnych tagów. Kliknij, aby rozpocząć."
none: "Nie dodałeś żadnych tagów."
click_to_get_started: "Kliknij tutaj, aby rozpocząć."
header_link_text: "Etykiety"
header_action_title: "edytuj tagi paska bocznego"
configure_defaults: "Skonfiguruj ustawienia domyślne"
categories:
+ links:
+ add_categories:
+ content: "Dodaj kategorie"
+ title: "Nie dodano żadnych kategorii. Kliknij, aby rozpocząć."
none: "Nie dodałeś żadnych kategorii."
click_to_get_started: "Kliknij tutaj, aby rozpocząć."
header_link_text: "Kategorie"
@@ -4197,8 +4256,10 @@ pl_PL:
review:
content: "Sprawdź"
title: "sprawdź"
+ pending_count: "%{count} oczekujących"
welcome_topic_banner:
title: "Stwórz swój temat powitalny"
+ description: "Twój temat powitalny to pierwsza rzecz, którą przeczytają nowi członkowie. Pomyśl o tym jako o swoim „przemówieniu” lub „deklaracji misji”. Poinformuj wszystkich, dla kogo jest ta społeczność, czego mogą się tu spodziewać, i co chcesz, aby zrobili najpierw."
button_title: "Rozpocznij edycję"
until: "Aż do:"
admin_js:
@@ -4479,7 +4540,6 @@ pl_PL:
go_back: "Powrót do listy"
payload_url: "Zawartość URL"
payload_url_placeholder: "https://example.com/postreceive"
- warn_local_payload_url: "Wygląda na to, że próbujesz skonfigurować webhook na lokalny adres URL. Event dostarczony na adres lokalny może spowodować efekt uboczny lub nieoczekiwane zachowanie. Kontynuować?"
secret_invalid: "Sekret nie może zawierać pustych znaków."
secret_too_short: "Sekret musi zawierać przynajmniej 12 znaków."
secret_placeholder: "Opcjonalna fraza użyta do generowania podpisu"
@@ -4524,6 +4584,7 @@ pl_PL:
name: "Zdarzenie powiadomienia"
details: "Kiedy użytkownik otrzymuje powiadomienie."
user_promoted_event:
+ name: "Wydarzenie promowania użytkownika"
details: "Gdy użytkownik awansuje z jednego poziomu zaufania na inny."
user_badge_event:
name: "Wydarzenie przyznania odznak"
@@ -4771,7 +4832,6 @@ pl_PL:
last_attempt: "Proces instalacji nie został zakończony, ostatnia próba:"
remote_branch: "Nazwa oddziału (opcjonalnie)"
public_key: "Podaj następujący klucz publiczny pozwalający na dostęp do repozytorium:"
- public_key_note: "Po wprowadzeniu powyżej poprawnego adresu URL prywatnego repozytorium klucz SSH zostanie wygenerowany i wyświetlony tutaj."
install: "Zainstaluj"
installed: "Zainstalowana"
install_popular: "Popularne"
@@ -4803,6 +4863,7 @@ pl_PL:
has_overwritten_history: "Bieżąca wersja motywu już nie istnieje, ponieważ historia Git została nadpisana przez wymuszony push."
add: "Dodaj"
theme_settings: "Ustawienia motywu"
+ overriden_settings_explanation: "Zastąpione ustawienia są oznaczone kropką i mają podświetlony kolor. Aby zresetować te ustawienia do wartości domyślnych, naciśnij przycisk resetowania obok nich."
no_settings: "Ten temat nie ma żanych ustawień"
theme_translations: "Tłumaczenia tematu"
empty: "Brak elementu"
@@ -5453,7 +5514,6 @@ pl_PL:
trust_level_2_users: "Użytkownicy o 2. poziomie zaufania"
trust_level_3_requirements: "Wymagania 3. poziomu zaufania"
trust_level_locked_tip: "poziom zaufania jest zablokowany, system nie będzie awansować lub degradować tego użytkownika"
- trust_level_unlocked_tip: "poziom zaufania jest odblokowany, system może awansować lub degradować tego użytkownika"
lock_trust_level: "Zablokuj poziom zaufania"
unlock_trust_level: "Odblokuj poziom zaufania"
silenced_count: "Wyciszony"
@@ -5515,23 +5575,23 @@ pl_PL:
delete_confirm: "Czy na pewno chcesz usunąć to pole użytkownika?"
options: "Opcje"
required:
- title: "Wymagane przy rejestracji?"
+ title: "Wymagane przy rejestracji"
enabled: "wymagane"
disabled: "niewymagane"
editable:
- title: "Edytowalne po rejestracji?"
+ title: "Edytowalne po rejestracji"
enabled: "edytowalne"
disabled: "nieedytowalne"
show_on_profile:
- title: "Widoczne w publicznym profilu?"
+ title: "Widoczne w publicznym profilu"
enabled: "widoczne w profilu"
disabled: "niewidoczne w profilu"
show_on_user_card:
- title: "Pokaż na karcie użytkownika?"
+ title: "Pokaż na karcie użytkownika"
enabled: "wyświetlane na karcie użytkownika"
disabled: "nie pokazany na karcie użytkownika"
searchable:
- title: "Możliwe do wyszukania?"
+ title: "Możliwe do wyszukania"
enabled: "możliwe do wyszukania"
disabled: "nie można wyszukiwać"
field_types:
diff --git a/config/locales/client.pt.yml b/config/locales/client.pt.yml
index 6374339d30..e00f20cf89 100644
--- a/config/locales/client.pt.yml
+++ b/config/locales/client.pt.yml
@@ -1049,7 +1049,6 @@ pt:
experimental_sidebar:
options: "Opções"
navigation_section: "Navegação"
- list_destination_default: "Predefinição"
change: "alterar"
featured_topic: "Tópico em Destaque"
moderator: "%{user} é um moderador"
@@ -4393,7 +4392,6 @@ pt:
trust_level_2_users: "Utilizadores no Nível de Confiança 2"
trust_level_3_requirements: "Requisitos do Nível de Confiança 3"
trust_level_locked_tip: "o Nível de Confiança está bloqueado, o sistema não irá promover ou despromover o utilizador"
- trust_level_unlocked_tip: "o Nível de Confiança está desbloqueado, o sistema poderá promover ou despromover o utilizador"
lock_trust_level: "Bloquear Nível de Confiança"
unlock_trust_level: "Desbloquear Nível de Confiança"
silenced_count: "Silenciado"
@@ -4446,19 +4444,19 @@ pt:
delete_confirm: "Tem a certeza que quer eliminar esse campo de utilizador?"
options: "Opções"
required:
- title: "Obrigatório na inscrição?"
+ title: "Obrigatório na inscrição"
enabled: "obrigatório"
disabled: "não obrigatório"
editable:
- title: "Editável depois da inscrição?"
+ title: "Editável depois da inscrição"
enabled: "editável"
disabled: "não editável"
show_on_profile:
- title: "Exibir no perfil público?"
+ title: "Exibir no perfil público"
enabled: "exibido no perfil"
disabled: "não exibido no perfil"
show_on_user_card:
- title: "Mostrar no cartão de utilizador?"
+ title: "Mostrar no cartão de utilizador"
enabled: "mostrar no cartão de utilizador"
disabled: "não apresentado no cartão de utilizador"
field_types:
diff --git a/config/locales/client.pt_BR.yml b/config/locales/client.pt_BR.yml
index 1351fb79b4..8f5c044d84 100644
--- a/config/locales/client.pt_BR.yml
+++ b/config/locales/client.pt_BR.yml
@@ -326,8 +326,8 @@ pt_BR:
list_permission_denied: "Você não tem permissão para visualizar os favoritos deste usuário(a)."
no_user_bookmarks: "Você não tem postagens favoritas. Os favoritos permitem que você consulte rapidamente postagens específicas."
auto_delete_preference:
- label: "Após ser notificado"
- never: "Manter Marcador"
+ label: "Após receber notificação"
+ never: "Manter marcador"
when_reminder_sent: "Excluir favorito"
on_owner_reply: "Excluir marcador, após responder"
clear_reminder: "Manter marcador e limpar lembretes"
@@ -974,7 +974,7 @@ pt_BR:
user_fields:
none: "(selecione uma opção)"
required: 'Digite um valor para "%{name}"'
- same_as_password: 'Sua senha não deve ser repetida em outros campos.'
+ same_as_password: "Sua senha não deve ser repetida em outros campos."
user:
said: "%{username}:"
profile: "Perfil"
@@ -1113,9 +1113,6 @@ pt_BR:
tags_section: "Seção de Etiquetas"
tags_section_instruction: "As etiquetas selecionadas serão exibidas na seção de etiquetas da barra lateral."
navigation_section: "Navegação"
- list_destination_instruction: "Quando eu clicar em um link de lista de tópicos na barra lateral com tópicos novos ou não lidos, leve-me para"
- list_destination_default: "Padrão"
- list_destination_unread_new: "Novo/Não lido"
change: "alterar"
featured_topic: "Tópico em destaque"
moderator: "%{user} é moderador(a)"
@@ -1336,7 +1333,7 @@ pt_BR:
upload_title: "Enviar sua imagem"
image_is_not_a_square: "Aviso: cortamos a sua imagem. A largura e a altura não eram iguais."
logo_small: "Logotipo pequeno do site. Usado por padrão."
- use_custom: "Ou envie um avatar customizado:"
+ use_custom: "Ou envie um avatar personalizado:"
change_profile_background:
title: "Cabeçalho do perfil"
instructions: "Os cabeçalhos do perfil serão centralizados e terão largura padrão de 1110px."
@@ -1671,7 +1668,7 @@ pt_BR:
the_topic: "o tópico"
user_status:
save: "Salvar"
- set_custom_status: "Definir status customizados"
+ set_custom_status: "Definir status personalizados"
what_are_you_doing: "O que você está fazendo?"
remove_status: "Remover status"
popup:
@@ -2111,7 +2108,7 @@ pt_BR:
slow_mode:
error: "Este tópico está no modo lento. Você já postou recentemente. É possível postar outra vez em %{timeLeft}."
user_not_seen_in_a_while:
- single: "A pessoa para quem você está enviando mensagens, %{usernames}, não é vista aqui há muito tempo – %{time_ago}. Eles podem não receber sua mensagem. Você pode procurar métodos alternativos de contato %{usernames}."
+ single: "A pessoa para quem você está enviando mensagens, %{usernames}, não é vista aqui há muito tempo – %{time_ago}. Elas podem não receber sua mensagem. Você pode procurar métodos alternativos de contato %{usernames}."
multiple: "As seguintes pessoas para quem você está enviando mensagens: %{usernames}, não são vistas aqui há muito tempo – %{time_ago}. Elas podem não receber sua mensagem. Você pode procurar métodos alternativos de contatá-las."
admin_options_title: "Configurações opcionais da equipe para este tópico"
composer_actions:
@@ -2411,10 +2408,10 @@ pt_BR:
likes_with_unread:
one: "Curtidas - %{count} curtida não lida"
other: "Curtidas - %{count} curtidas não lidas"
- watching: "Tópicos assistidos"
+ watching: "Tópicos acompanhados"
watching_with_unread:
- one: "Tópicos assistidos - %{count} tópico assistido não lido"
- other: "Tópicos assistidos - %{count} tópicos assistidos não lidos"
+ one: "Tópicos acompanhados - %{count} tópico acompanhado não lido"
+ other: "Tópicos acompanhados - %{count} tópicos assistidos não lidos"
messages: "Mensagens pessoais"
messages_with_unread:
one: "Mensagens pessoais - %{count} mensagem não lida"
@@ -3183,7 +3180,7 @@ pt_BR:
description: "Exigir que novos tópicos tenham etiquetas de grupos de etiquetas:"
delete: "Excluir"
add: "Adicionar grupo de tags necessário"
- placeholder: "selecionar grupo de tags..."
+ placeholder: "selecionar grupo de etiquetas..."
topic_featured_link_allowed: "Permitir links em destaque nesta categoria"
delete: "Excluir categoria"
create: "Nova categoria"
@@ -3862,9 +3859,9 @@ pt_BR:
inbox: "Caixa de entrada"
sent: "Enviadas"
new: "Novo"
- new_with_count: "Novo (%{count})"
+ new_with_count: "Novos(s) (%{count})"
unread: "Não lidos(as)"
- unread_with_count: "Não lido (%{count})"
+ unread_with_count: "Não lido(s) (%{count})"
archive: "Arquivo"
tags:
none: "Você não adicionou nenhuma etiqueta."
@@ -3895,7 +3892,7 @@ pt_BR:
content: "FAQ"
tracked:
content: "Monitorados(as)"
- title: "Todos os Tópicos Rastreados"
+ title: "Todos os tópicos monitorados"
groups:
content: "Grupos"
title: "Todos os grupos"
@@ -3904,7 +3901,7 @@ pt_BR:
title: "Todos(as) os(as) usuários(as)"
my_posts:
content: "Minhas postagens"
- title: "Minhas publicações"
+ title: "Minhas postagens"
draft_count:
one: "%{count} rascunho"
other: "%{count} rascunhos"
@@ -3913,8 +3910,8 @@ pt_BR:
title: "revisar"
pending_count: "%{count} pendente"
welcome_topic_banner:
- title: "Crie seu tópico de Boas-Vindas"
- description: 'Seu tópico de boas-vindas é a primeira coisa que os novos membros vão ler. Pense nisso como seu “argumento de elevador” ou “declaração de missão”. Informe a todos sobre quem é o público desta comunidade, o que eles podem esperar encontrar aqui e o que você gostaria que eles fizessem primeiro.'
+ title: "Crie seu Tópico de Boas-Vindas"
+ description: "Seu tópico de boas-vindas é a primeira coisa que os novos membros vão ler. Pense nisso como seu “argumento de elevador” ou “declaração de missão”. Informe a todos sobre quem é o público desta comunidade, o que eles podem esperar encontrar aqui e o que você gostaria que eles fizessem primeiro."
button_title: "Começar a Editar"
until: "Até:"
admin_js:
@@ -4189,7 +4186,6 @@ pt_BR:
go_back: "Voltar para a lista"
payload_url: "URL do conteúdo"
payload_url_placeholder: "https://example.com/postreceive"
- warn_local_payload_url: "Parece que você está tentando configurar um webhook para uma URL local. Eventos entregues a endereços locais podem causar efeitos colaterais ou comportamentos inesperados. Continuar?"
secret_invalid: "O segredo não deve conter caracteres em branco."
secret_too_short: "O segredo deve ter pelo menos 12 caracteres."
secret_placeholder: "Uma linha opcional, usado para gerar a assinatura"
@@ -4478,7 +4474,6 @@ pt_BR:
last_attempt: "O processo de instalação não foi concluído, última tentativa:"
remote_branch: "Nome da unidade (opcional)"
public_key: "Conceder o seguinte acesso da chave pública ao repo:"
- public_key_note: "Após digitar uma URL do repositório privada válida, uma chave SSH será gerada e exibida aqui."
install: "Instalar"
installed: "Instalado(a)"
install_popular: "Mais acessados(as)"
@@ -5157,7 +5152,6 @@ pt_BR:
trust_level_2_users: "Usuários(as) de nível de confiança 2"
trust_level_3_requirements: "Requisitos de nível de confiança 3"
trust_level_locked_tip: "o nível de confiança está bloqueado, o sistema não irá promover ou rebaixará o(a) usuário(a)"
- trust_level_unlocked_tip: "o nível de confiança está desbloqueado, o sistema poderá promover ou rebaixar o(a) usuário(a)"
lock_trust_level: "Bloquear nível de confiança"
unlock_trust_level: "Desbloquear nível de confiança"
silenced_count: "Silenciado(a)"
@@ -5217,23 +5211,18 @@ pt_BR:
delete_confirm: "Tem certeza que quer excluir este campo de usuário(a)?"
options: "Opções"
required:
- title: "Necessário para cadastro?"
enabled: "necessário(a)"
disabled: "não necessário(a)"
editable:
- title: "Editável após criar conta?"
enabled: "editável"
disabled: "não editável"
show_on_profile:
- title: "Exibir no perfil público?"
enabled: "exibido(a) no perfil"
disabled: "não exibido(a) no perfil"
show_on_user_card:
- title: "Exibir no cartão de usuário(a)?"
enabled: "exibir no cartão de usuário(a)"
disabled: "não exibido no cartão de usuário(a)"
searchable:
- title: "Pesquisável?"
enabled: "pesquisável"
disabled: "não pesquisável"
field_types:
diff --git a/config/locales/client.ro.yml b/config/locales/client.ro.yml
index 9b79d81278..2434fb07ef 100644
--- a/config/locales/client.ro.yml
+++ b/config/locales/client.ro.yml
@@ -1017,7 +1017,6 @@ ro:
experimental_sidebar:
options: "Opțiuni"
navigation_section: "Navigare"
- list_destination_default: "Implicit"
change: "Schimbă"
featured_topic: "Subiect recomandat"
moderator: "%{user} este moderator"
@@ -3721,7 +3720,6 @@ ro:
trust_level_2_users: "utilizatori de nivel de încredere 2 "
trust_level_3_requirements: "Cerințe pentru nivelul 3 de încredere"
trust_level_locked_tip: "Nivelul de încredere este blocat, sistemul nu va promova sau retrograda utilizatorii"
- trust_level_unlocked_tip: "Nivelul de încredere este deblocat, sistemul poate promova sau retrograda utilizatorii"
lock_trust_level: "Blochează nivelul de încredere"
unlock_trust_level: "Deblochează nivelul de încredere"
suspended_count: "Suspendați"
@@ -3774,19 +3772,19 @@ ro:
delete_confirm: "Ești sigur că vrei să ștergi acest câmp utilizator?"
options: "Opțiuni"
required:
- title: "Necesar la înscriere?"
+ title: "Necesar la înscriere"
enabled: "necesar"
disabled: "opţional"
editable:
- title: "Editabil după înregistrare?"
+ title: "Editabil după înregistrare"
enabled: "editabil"
disabled: "nu este editabil"
show_on_profile:
- title: "Arată în profilul public?"
+ title: "Arată în profilul public"
enabled: "se afișează în profil"
disabled: "nu se afișează în profil"
show_on_user_card:
- title: "Afișează pe pagina cu date personale utilizatorului?"
+ title: "Afișează pe pagina cu date personale utilizatorului"
enabled: "se afișează"
disabled: "nu se afișează"
field_types:
diff --git a/config/locales/client.ru.yml b/config/locales/client.ru.yml
index c17ec0b55a..67c9eb32b9 100644
--- a/config/locales/client.ru.yml
+++ b/config/locales/client.ru.yml
@@ -35,9 +35,9 @@ ru:
long_no_year_no_time: "D MMM"
full_no_year_no_time: "D MMM"
long_with_year: "D MMM YYYY, HH:mm"
- long_with_year_no_time: "D MMM, YYYY"
- full_with_year_no_time: "LL"
- long_date_with_year: "D MMM YY, LT"
+ long_with_year_no_time: "D MMM YYYY"
+ full_with_year_no_time: "D MMMM YYYY"
+ long_date_with_year: "D MMM YYYY, LT"
long_date_without_year: "D MMM, LT"
long_date_with_year_without_time: "D MMM YYYY"
long_date_without_year_with_linebreak: "D MMM
LT"
@@ -45,57 +45,57 @@ ru:
wrap_ago: "%{date} назад"
wrap_on: "%{date}"
tiny:
- half_a_minute: "< 1мин"
+ half_a_minute: "< 1 мин"
less_than_x_seconds:
- one: "< %{count}сек"
- few: "< %{count}сек"
- many: "< %{count}сек"
- other: "< %{count}с"
+ one: "< %{count} с"
+ few: "< %{count} с"
+ many: "< %{count} с"
+ other: "< %{count} с"
x_seconds:
- one: "%{count}с"
- few: "%{count}с"
- many: "%{count}с"
- other: "%{count}с"
+ one: "%{count} с"
+ few: "%{count} с"
+ many: "< %{count} с"
+ other: "< %{count} с"
less_than_x_minutes:
- one: "< %{count}мин"
- few: "< %{count}м"
- many: "< %{count}м"
- other: "< %{count}мин"
+ one: "< %{count} мин"
+ few: "< %{count} мин"
+ many: "< %{count} мин"
+ other: "< %{count} мин"
x_minutes:
- one: "%{count}м"
- few: "%{count}мин"
- many: "%{count}мин"
- other: "%{count}мин"
+ one: "%{count} мин"
+ few: "%{count} мин"
+ many: "%{count} мин"
+ other: "%{count} мин"
about_x_hours:
- one: "%{count}ч"
- few: "%{count}ч"
- many: "%{count}ч"
- other: "%{count}ч"
+ one: "%{count} ч"
+ few: "%{count} ч"
+ many: "%{count} ч"
+ other: "%{count} ч"
x_days:
- one: "%{count}д"
- few: "%{count}д"
- many: "%{count}д"
- other: "%{count}д"
+ one: "%{count} д"
+ few: "%{count} дн"
+ many: "%{count} дн"
+ other: "%{count} дн"
x_months:
- one: "%{count}мес"
- few: "%{count}мес"
- many: "%{count}мес"
- other: "%{count}мес"
+ one: "%{count} мес"
+ few: "%{count} мес"
+ many: "%{count} мес"
+ other: "%{count} мес"
about_x_years:
- one: "%{count}год"
- few: "%{count}года"
- many: "%{count}лет"
- other: "%{count}лет"
+ one: "%{count} год"
+ few: "%{count} года"
+ many: "%{count} лет"
+ other: "%{count} года"
over_x_years:
- one: "> %{count}года"
- few: "> %{count}лет"
- many: "> %{count}лет"
- other: "> %{count}лет"
+ one: "> %{count} года"
+ few: "> %{count} лет"
+ many: "> %{count} лет"
+ other: "> %{count} года"
almost_x_years:
- one: "%{count}год"
- few: "%{count}года"
- many: "%{count}лет"
- other: "%{count}лет"
+ one: "%{count} год"
+ few: "%{count} года"
+ many: "%{count} лет"
+ other: "%{count} года"
date_month: "D MMM"
date_year: "MMM YYYY"
medium:
@@ -103,7 +103,7 @@ ru:
one: "менее %{count} минуты назад"
few: "менее %{count} минут назад"
many: "менее %{count} минут назад"
- other: "менее %{count} минут назад"
+ other: "менее %{count} минуты назад"
x_minutes:
one: "%{count} мин"
few: "%{count} мин"
@@ -113,37 +113,37 @@ ru:
one: "%{count} час"
few: "%{count} часа"
many: "%{count} часов"
- other: "%{count} часов"
+ other: "%{count} часа"
about_x_hours:
one: "около %{count} часа"
few: "около %{count} часов"
many: "около %{count} часов"
- other: "около %{count} часов"
+ other: "около %{count} часа"
x_days:
one: "%{count} день"
few: "%{count} дня"
many: "%{count} дней"
- other: "%{count} дней"
+ other: "%{count} дня"
x_months:
one: "%{count} месяц"
few: "%{count} месяца"
many: "%{count} месяцев"
- other: "%{count} месяцев"
+ other: "%{count} месяца"
about_x_years:
one: "около %{count} года"
few: "около %{count} лет"
many: "около %{count} лет"
- other: "около %{count} лет"
+ other: "около %{count} года"
over_x_years:
one: "более %{count} года"
few: "более %{count} лет"
many: "более %{count} лет"
- other: "более %{count} лет"
+ other: "более %{count} года"
almost_x_years:
one: "почти %{count} год"
few: "почти %{count} года"
many: "почти %{count} лет"
- other: "почти %{count} лет"
+ other: "почти %{count} года"
date_year: "D MMM, YYYY"
medium_with_ago:
x_minutes:
@@ -202,7 +202,7 @@ ru:
url: "Копировать и поделиться ссылкой"
action_codes:
public_topic: "Сделал тему публичной %{when}"
- open_topic: "converted this to a topic %{when}"
+ open_topic: "преобразовал это в тему %{when}"
private_topic: "Сделал тему личным сообщением %{when}"
split_topic: "Разделил эту тему %{when}"
invited_user: "Пригласил %{who} %{when}"
@@ -1088,7 +1088,7 @@ ru:
user_fields:
none: "(выберите)"
required: 'Пожалуйста, введите значение для "%{name}"'
- same_as_password: 'Указанный пароль не должен фигурировать в других полях.'
+ same_as_password: "Указанный пароль не должен фигурировать в других полях."
user:
said: "%{username}:"
profile: "Профиль"
@@ -1227,9 +1227,6 @@ ru:
tags_section: "Теги"
tags_section_instruction: "Выбранные теги будут отображаться в соответствующей секции боковой панели."
navigation_section: "Навигация"
- list_destination_instruction: "При нажатии на ссылку списка тем на боковой панели с новыми или непрочитанными темами открывать"
- list_destination_default: "Домашнюю страницу"
- list_destination_unread_new: "Новые/Непрочитанные темы"
change: "изменить"
featured_topic: "Избранная тема"
moderator: "%{user} — модератор"
@@ -4278,7 +4275,7 @@ ru:
pending_count: "В ожидании: %{count}"
welcome_topic_banner:
title: "Создать приветственную тему"
- description: 'Ваша приветственная тема — это первое, что прочитают новички. Постарайтесь максимально коротко и ярко выразить в ней наиболее важную информацию, которую вы хотите донести до новых пользователей форума.'
+ description: "Ваша приветственная тема — это первое, что прочитают новички. Постарайтесь максимально коротко и ярко выразить в ней наиболее важную информацию, которую вы хотите донести до новых пользователей форума."
button_title: "Начать редактирование"
until: "До:"
admin_js:
@@ -4559,7 +4556,6 @@ ru:
go_back: "Вернуться к списку"
payload_url: "Ссылка для отправки"
payload_url_placeholder: "https://example.com/postreceive"
- warn_local_payload_url: "По-видимому вы пытаетесь настроить вебхук на локальный URL. Событие, отправляемое на локальный адрес, может иметь побочное действие или неожиданное поведение. Продолжить?"
secret_invalid: "Ключ не должен содержать пробелов."
secret_too_short: "Ключ должен быть не менее 12 символов."
secret_placeholder: "Создание подписи (необязательно)"
@@ -4852,7 +4848,6 @@ ru:
last_attempt: "Процесс установки не завершен, последняя попытка:"
remote_branch: "Имя ветки (необязательно)"
public_key: "Предоставьте доступ к репозиторию со следующим открытым ключом:"
- public_key_note: "После ввода корректного URL-адреса приватного репозитория, будет сгенерирован ключ SSH, который будет отображаться здесь."
install: "Установить"
installed: "Установленные"
install_popular: "Популярные"
@@ -5533,7 +5528,6 @@ ru:
trust_level_2_users: "Пользователи с 2 уровнем доверия"
trust_level_3_requirements: "Требования для 3 уровня доверия"
trust_level_locked_tip: "Уровень доверия заблокирован, система не сможет изменять уровень доверия пользователя"
- trust_level_unlocked_tip: "Уровень доверия разблокирован, система сможет изменять уровень доверия пользователя"
lock_trust_level: "Заблокировать изменение уровня доверия"
unlock_trust_level: "Разблокировать изменение уровня доверия"
silenced_count: "Заблокированные"
@@ -5595,23 +5589,18 @@ ru:
delete_confirm: "Вы действительно хотите удалить это поле?"
options: "Парамерты"
required:
- title: "Обязательное при регистрации?"
enabled: "Обязательное"
disabled: "Необязательное"
editable:
- title: "Редактируемое после регистрации?"
enabled: "Редактируемое"
disabled: "Нередактируемое"
show_on_profile:
- title: "Показывать в публичном профиле?"
enabled: "Показывать в профиле"
disabled: "Не показывать в профиле"
show_on_user_card:
- title: "Показывать в карточке пользователя?"
enabled: "Показывать в карточке пользователя"
disabled: "Не показывать в карточке пользователя"
searchable:
- title: "Доступно для поиска?"
enabled: "Доступно для поиска"
disabled: "Не доступно для поиска"
field_types:
diff --git a/config/locales/client.sk.yml b/config/locales/client.sk.yml
index 3c44dae813..19027ef8ae 100644
--- a/config/locales/client.sk.yml
+++ b/config/locales/client.sk.yml
@@ -732,7 +732,6 @@ sk:
experimental_sidebar:
options: "Možnosti"
navigation_section: "Navigácia"
- list_destination_default: "Predvolené"
change: "zmeniť"
moderator: "%{user} je moderátor"
admin: "%{user} je administrátor"
@@ -2942,7 +2941,6 @@ sk:
trust_level_2_users: "Používatelia na stupni dôvery 2"
trust_level_3_requirements: "Požiadavky pre 3 stupeň dôvery"
trust_level_locked_tip: "stupeň dôvery je zamknutý, systém používateľovi stupeň nezvýši ani nezníži "
- trust_level_unlocked_tip: "stupeň dôvery je odomknutý, systém môže používateľovi stupeň zvýšiť alebo znížiť"
lock_trust_level: "Zamknúť stupeň dôvery"
unlock_trust_level: "Odomknúť stupeň dôvery"
suspended_count: "Odobrate práva"
@@ -2991,15 +2989,15 @@ sk:
delete_confirm: "Ste si istý, že chcete zmazať toto používateľské pole?"
options: "Možnosti"
required:
- title: "Požadované pri registrácii?"
+ title: "Požadované pri registrácii"
enabled: "povinné"
disabled: "nepovinné"
editable:
- title: "Upravovateľné po registrácii?"
+ title: "Upravovateľné po registrácii"
enabled: "upravovateľné "
disabled: "neupravovateľné "
show_on_profile:
- title: "Ukázať na verejnom profile?"
+ title: "Ukázať na verejnom profile"
enabled: "zobrazené na profile"
disabled: "nezobrazené na profile"
field_types:
diff --git a/config/locales/client.sl.yml b/config/locales/client.sl.yml
index d619528a9b..03f1b3697b 100644
--- a/config/locales/client.sl.yml
+++ b/config/locales/client.sl.yml
@@ -1022,7 +1022,6 @@ sl:
experimental_sidebar:
options: "Možnosti"
navigation_section: "Navigacija"
- list_destination_default: "Privzeto"
change: "spremeni"
featured_topic: "Izpostavljena tema"
moderator: "%{user} je moderator"
@@ -4099,7 +4098,6 @@ sl:
trust_level_2_users: "Uporabniki na nivoju zaupanja 2"
trust_level_3_requirements: "Zahteve za nivo zaupanja 3"
trust_level_locked_tip: "nivo zaupanja je zaklenjen, sistem ne bo spreminjal nivo zaupanja uporabnika"
- trust_level_unlocked_tip: "nivo zaupanja je odklenjen, sistem bo lahko spreminjal nivo zaupanja uporabnika"
lock_trust_level: "Zakleni nivo zaupanja"
unlock_trust_level: "Odkleni nivo zaupanja"
silenced_count: "Utišani"
@@ -4155,19 +4153,19 @@ sl:
delete_confirm: "Ali ste prepričani, da želite izbrisati to polje?"
options: "Možnosti"
required:
- title: "Zahtevano ob prijavi?"
+ title: "Zahtevano ob prijavi"
enabled: "zahtevano"
disabled: "ni zahtevano"
editable:
- title: "Uredljivo po prijavi?"
+ title: "Uredljivo po prijavi"
enabled: "uredljivo"
disabled: "ni uredljivo"
show_on_profile:
- title: "Prikaži na javnem profilu?"
+ title: "Prikaži na javnem profilu"
enabled: "prikazano na profilu"
disabled: "ni prikazano na profilu"
show_on_user_card:
- title: "Prikaži na uporabnikovi izkaznici?"
+ title: "Prikaži na uporabnikovi izkaznici"
enabled: "prikazano na uporabnikovi izkaznici"
disabled: "ni prikazano na uporabnikovi izkaznici"
field_types:
diff --git a/config/locales/client.sq.yml b/config/locales/client.sq.yml
index d091a7f54c..0881f07f5d 100644
--- a/config/locales/client.sq.yml
+++ b/config/locales/client.sq.yml
@@ -554,7 +554,6 @@ sq:
experimental_sidebar:
options: "Opsione"
navigation_section: "Shfletimi"
- list_destination_default: "Paracaktuar"
change: "ndrysho"
moderator: "%{user} është moderator"
admin: "%{user} është admin"
@@ -2549,7 +2548,6 @@ sq:
trust_level_2_users: "Përdorues me Nivel Besimi 2"
trust_level_3_requirements: "Kërkesat për përdorues me Nivel Besimi 3"
trust_level_locked_tip: "trust level is locked, system will not promote or demote user"
- trust_level_unlocked_tip: "trust level is unlocked, system will may promote or demote user"
lock_trust_level: "Kyç nivelin e besimit"
unlock_trust_level: "Çkyç nivelin e besimit"
suspended_count: "Të pezulluar"
@@ -2601,19 +2599,18 @@ sq:
delete_confirm: "Are you sure you want to delete that user field?"
options: "Opsione"
required:
- title: "Required at signup?"
+ title: "Required at signup"
enabled: "i nevojshëm"
disabled: "fakultativ"
editable:
- title: "Editable after signup?"
+ title: "Editable after signup"
enabled: "e modifikueshme"
disabled: "jo e modifikueshme"
show_on_profile:
- title: "Trego në profilin publik"
enabled: "e treguar në profilin publik"
disabled: "nuk tregohet në profilin publik"
show_on_user_card:
- title: "Trego në kartën e anëtarit?"
+ title: "Trego në kartën e anëtarit"
enabled: "e treguar në kartën e anëtarit"
disabled: "nuk tregohet në kartën e anëtarit"
field_types:
diff --git a/config/locales/client.sr.yml b/config/locales/client.sr.yml
index 9eecb8f87c..098826baf1 100644
--- a/config/locales/client.sr.yml
+++ b/config/locales/client.sr.yml
@@ -697,7 +697,6 @@ sr:
experimental_sidebar:
options: "Opcije"
navigation_section: "Navigacija"
- list_destination_default: "zadato"
change: "promeni"
moderator: "%{user} je moderator"
admin: "%{user} je administrator"
@@ -2329,7 +2328,6 @@ sr:
trust_level_2_users: "Korisnici Na Nivou Poverenja 2"
trust_level_3_requirements: "Predispozicije Za Nivo Poverenja 3"
trust_level_locked_tip: "Nivo poverenja zaključan, sistem neće unapređivati ili skidati korisnika"
- trust_level_unlocked_tip: "Nivo poverenja zaključan, sistem neće unapređivati ili skidati korisnika"
lock_trust_level: "Zaključaj Nivo Poverenja"
unlock_trust_level: "Odključaj Nivo Poverenja"
suspended_count: "Suspendovani"
@@ -2378,15 +2376,15 @@ sr:
delete_confirm: "Jeste li sigurni da želite obrisati to korisničko polje?"
options: "Opcije"
required:
- title: "Potrebno pri registraciji?"
+ title: "Potrebno pri registraciji"
enabled: "potrebno"
disabled: "nepotrebno"
editable:
- title: "Izmenljivo nakon registracije?"
+ title: "Izmenljivo nakon registracije"
enabled: "izmenljivo"
disabled: "nije izmenljivo"
show_on_profile:
- title: "Pokaži na javnom profilu?"
+ title: "Pokaži na javnom profilu"
enabled: "pokaži na profilu"
disabled: "nije prikazano na profilu"
field_types:
diff --git a/config/locales/client.sv.yml b/config/locales/client.sv.yml
index 4d8be59a13..5eae85842a 100644
--- a/config/locales/client.sv.yml
+++ b/config/locales/client.sv.yml
@@ -232,6 +232,7 @@ sv:
delete: "Radera"
generic_error: "Tyvärr, ett fel har inträffat."
generic_error_with_reason: "Ett fel inträffade: %{error}"
+ multiple_errors: "Flera fel inträffade: %{errors}"
sign_up: "Registrera"
log_in: "Logga in"
age: "Ålder"
@@ -974,7 +975,7 @@ sv:
user_fields:
none: "(välj ett alternativ)"
required: 'Ange ett värde för "%{name}"'
- same_as_password: 'Upprepa inte ditt lösenord i andra fält.'
+ same_as_password: "Upprepa inte ditt lösenord i andra fält."
user:
said: "%{username}:"
profile: "Profil"
@@ -1113,9 +1114,9 @@ sv:
tags_section: "Sektion Taggar"
tags_section_instruction: "Valda taggar kommer att visas under sidofältets tagg-sektion."
navigation_section: "Navigering"
- list_destination_instruction: "När jag klickar på en länk till en ämneslista i sidofältet med nya eller olästa ämnen kommer jag till"
- list_destination_default: "Standard"
- list_destination_unread_new: "Nya/olästa"
+ list_destination_instruction: "När det finns nytt innehåll i sidofältet..."
+ list_destination_default: "använd standardlänken och visa en utmärkelse för nya objekt"
+ list_destination_unread_new: "länka till oläst/nytt och visa ett antal nya objekt"
change: "ändra"
featured_topic: "Utvalt ämne"
moderator: "%{user} är en moderator"
@@ -1904,6 +1905,8 @@ sv:
success: "Ditt konto har skapats och du är nu inloggad."
name_label: "Namn"
password_label: "Lösenord"
+ existing_user_can_redeem: "Lös in din inbjudan till ett ämne eller en grupp."
+ existing_user_cannot_redeem: "Denna inbjudan kan inte lösas in. Be personen som bjöd in dig att skicka en ny inbjudan till dig."
password_reset:
continue: "Fortsätt till %{site_name}"
emoji_set:
@@ -3867,12 +3870,20 @@ sv:
unread_with_count: "Oläst (%{count})"
archive: "Arkiv"
tags:
+ links:
+ add_tags:
+ content: "Lägg till taggar"
+ title: "Du har inte lagt till några taggar. Klicka för att komma igång."
none: "Du har inte lagt till några taggar."
click_to_get_started: "Klicka här för att komma igång."
header_link_text: "Taggar"
header_action_title: "redigera sidofältets taggar"
configure_defaults: "Konfigurera standardvärden"
categories:
+ links:
+ add_categories:
+ content: "Lägg till kategorier"
+ title: "Du har inte lagt till några kategorier. Klicka för att komma igång."
none: "Du har inte lagt till några kategorier."
click_to_get_started: "Klicka här för att komma igång."
header_link_text: "Kategorier"
@@ -3914,7 +3925,7 @@ sv:
pending_count: "%{count} väntande"
welcome_topic_banner:
title: "Skapa ditt välkomstämne"
- description: 'Ditt välkomstämne är det första som nya medlemmar kommer att läsa. Tänk på det som din "snabbpresentation" eller "målbeskrivning". Låt alla veta vem den här gruppen är till för, vad de kan förvänta sig att hitta här och vad du vill att de ska göra först.'
+ description: "Ditt välkomstämne är det första som nya medlemmar kommer att läsa. Tänk på det som din \"snabbpresentation\" eller \"målbeskrivning\". Låt alla veta vem den här gruppen är till för, vad de kan förvänta sig att hitta här och vad du vill att de ska göra först."
button_title: "Börja redigera"
until: "T.o.m.:"
admin_js:
@@ -4189,7 +4200,6 @@ sv:
go_back: "Tillbaka till listan"
payload_url: "Försändelse-URL"
payload_url_placeholder: "https://example.com/postreceive"
- warn_local_payload_url: "Det verkar som du försöker upprätta en webhook till en lokal URL. Händelser levererade till en lokal adress kan orsaka bieffekter eller oväntad funktion. Vill du fortsätta?"
secret_invalid: "Hemligheten får inte ha några blanka tecken."
secret_too_short: "Hemligheten bör vara minst 12 tecken."
secret_placeholder: "En alternativ sträng som används för att generera signatur"
@@ -4288,6 +4298,7 @@ sv:
change_settings_short: "Inställningar"
howto: "Hur installerar jag tillägg?"
official: "Officiellt tillägg"
+ broken_route: "Det går inte att konfigurera länken till '%{name}'. Se till att annonsblockerare är inaktiverade och försök ladda om sidan."
backups:
title: "Säkerhetskopior"
menu:
@@ -4478,7 +4489,6 @@ sv:
last_attempt: "Installationsprocessen slutfördes inte, det senaste försöket:"
remote_branch: "Filialnamn (valfritt)"
public_key: "Ge följande offentliga nyckel tillträde till lagringsplatsen:"
- public_key_note: "När du har angett en giltig privat lagringsplats URL ovan genereras det en SSH-nyckel som visas här."
install: "Installera"
installed: "Installerad"
install_popular: "Populära"
@@ -5157,7 +5167,7 @@ sv:
trust_level_2_users: "Användare med förtroendenivå 2"
trust_level_3_requirements: "Krav för förtroendenivå 3"
trust_level_locked_tip: "förtroendenivå är låst, systemet kommer ej att befordra eller degradera användare"
- trust_level_unlocked_tip: "förtroendenivå är olåst, systemet kan komma att befordra eller degradera användare"
+ trust_level_unlocked_tip: "förtroendenivån är olåst, systemet kan befordra eller degradera användare"
lock_trust_level: "Lås förtroendenivå"
unlock_trust_level: "Lås upp förtroendenivå"
silenced_count: "Tystad"
@@ -5217,23 +5227,23 @@ sv:
delete_confirm: "Är du säker på att du vill ta bort det här användarfältet?"
options: "Alternativ"
required:
- title: "Krävs vid registrering?"
+ title: "Krävs vid registrering"
enabled: "krävs"
disabled: "krävs ej"
editable:
- title: "Redigerbar efter registrering?"
+ title: "Redigerbar efter registrering"
enabled: "redigerbar"
disabled: "ej redigerbar"
show_on_profile:
- title: "Visa på offentlig profil?"
+ title: "Visa på offentlig profil"
enabled: "visas på profil"
disabled: "visas ej på profil"
show_on_user_card:
- title: "Ska visas på användarkort?"
+ title: "Ska visas på användarkort"
enabled: "visas på användarkort"
disabled: "visas inte på användarkort"
searchable:
- title: "Sökbart?"
+ title: "Sökbart"
enabled: "sökbart"
disabled: "inte sökbart"
field_types:
diff --git a/config/locales/client.sw.yml b/config/locales/client.sw.yml
index 05ad3bd505..9f748dc407 100644
--- a/config/locales/client.sw.yml
+++ b/config/locales/client.sw.yml
@@ -645,7 +645,6 @@ sw:
experimental_sidebar:
options: "Chaguo"
navigation_section: "Abiri"
- list_destination_default: "Halisi"
change: "badilisha"
moderator: "%{mtumiaji} ni msimamizi"
admin: "%{mtumiaji} ni kiongozi"
@@ -3079,7 +3078,6 @@ sw:
trust_level_2_users: "Watumiaji wenye Kiwango cha 2 cha Uaminifu"
trust_level_3_requirements: "Mahitaji ya Kiwango cha 3 cha uaminifu"
trust_level_locked_tip: "kiwango cha uaminifu kimefungwa, mfumo hauta mvusha au kumshusha mtu daraja"
- trust_level_unlocked_tip: "kiwango cha uaminifu kimefunguliwa, mfumo unaweza kumvusha au kumshusha mtu daraja"
lock_trust_level: "Funga Kiwango cha Uaminifu"
unlock_trust_level: "Fungua Kiwango cha Uaminifu"
silenced_count: "Amenyamazishwa"
@@ -3129,19 +3127,19 @@ sw:
delete_confirm: "Una uhakika unataka kufuta sehemu ya taarifa ya mtumiaji?"
options: "Machaguo"
required:
- title: "Inahitajika wakati wa kujiunga?"
+ title: "Inahitajika wakati wa kujiunga"
enabled: "muhimu"
disabled: "sio muhimu"
editable:
- title: "Inaweza kufanyiwa uhariri baada ya kujiunga?"
+ title: "Inaweza kufanyiwa uhariri baada ya kujiunga"
enabled: "inaweza kufanyiwa uhariri"
disabled: "haiwezi kufanyiwa uhariri"
show_on_profile:
- title: "Imeonyeshwa kwenye maelezo mafupi ya mtumiaji yanayo onwa na umma?"
+ title: "Imeonyeshwa kwenye maelezo mafupi ya mtumiaji yanayo onwa na umma"
enabled: "imeonyeshwa kwenye maelezo mafupi ya mtumiaji"
disabled: "haijaonyeshwa kwenye maelezo mafupi ya mtumiaji"
show_on_user_card:
- title: "Onyesha kwenye kadi ya mtumiaji?"
+ title: "Onyesha kwenye kadi ya mtumiaji"
enabled: "onyesha kwenye kadi ya mtumiaji"
disabled: "usioneshe kwenye kadi ya mtumiaji"
field_types:
diff --git a/config/locales/client.te.yml b/config/locales/client.te.yml
index 2221a9dd15..e8b5ff8ed1 100644
--- a/config/locales/client.te.yml
+++ b/config/locales/client.te.yml
@@ -415,7 +415,6 @@ te:
experimental_sidebar:
options: "ఎంపికలు"
navigation_section: "నావిగేషను"
- list_destination_default: "అప్రమేయ"
change: "మార్చు"
moderator: "%{user} ఒక నిర్వాహకుడు"
admin: "%{user} ఒక అధికారి"
@@ -1872,7 +1871,6 @@ te:
trust_level_2_users: "నమ్మకం స్థాయి 2 సభ్యులు"
trust_level_3_requirements: "నమ్మకపు స్థాయి 3 అవసరాలు"
trust_level_locked_tip: "నమ్మకపు స్థాయి బంధింపబడిఉంది, వ్యవస్థ వినియోగదారుని ప్రోత్సాహించలేదు లేదా స్థాయి తగ్గించలేదు"
- trust_level_unlocked_tip: "నమ్మకపు స్థాయి బంధింపబడలేదు, వ్యవస్థ వినియోగదారుని ప్రోత్సాహించవచ్చు లేదా స్థాయి తగ్గించవచ్చు"
lock_trust_level: "నమ్మకపు స్థాయి ని బంధించు"
unlock_trust_level: "నమ్మకపు స్థాయిని వదిలేయి"
suspended_count: "సస్పెడయ్యాడు"
@@ -1921,15 +1919,12 @@ te:
delete_confirm: "మీరు నిజంగా ఈ సభ్య క్షేత్రం తొలగించాలనుకుంటున్నారా?"
options: "ఎంపికలు"
required:
- title: "సైన్అప్ అవసరమా?"
enabled: "కావాలి"
disabled: "అవసరంలేదు"
editable:
- title: "సైన్అప్ తరువాత సవరించగలమా?"
enabled: "సవరించదగిన"
disabled: "సవరించలేని"
show_on_profile:
- title: "ప్రజా ప్రవరపై చూపు?"
enabled: "ప్రవరపై చూపు"
disabled: "ప్రవరపై చూపబడలేదు"
field_types:
diff --git a/config/locales/client.th.yml b/config/locales/client.th.yml
index dfa311e31b..bff6036afc 100644
--- a/config/locales/client.th.yml
+++ b/config/locales/client.th.yml
@@ -782,7 +782,6 @@ th:
experimental_sidebar:
options: "ตัวเลือก"
navigation_section: "การนำทาง"
- list_destination_default: "ค่าเริ่มต้น"
change: "เปลี่ยนแปลง"
featured_topic: "กระทู้เด่น"
moderator: "%{user} เป็นผู้ดูแลระบบ"
@@ -2994,7 +2993,6 @@ th:
trust_level_2_users: "ระดับความไว้ใจ 2 ผู้ใช้"
trust_level_3_requirements: "ระดับความไว้ใจ 3 ความต้องการ"
trust_level_locked_tip: "ระดับความไว้ใจถูกล็อกไว้ระบบจะไม่ปรับระดับความไว้ใจของผู้ใช้ขึ้นหรือลง"
- trust_level_unlocked_tip: "ระดับความไว้ใจถูกปลดล็อก ระบบจะปรับระดับความไว้ใจขึ้นหรือลงของผู้ใช้ตามปกติ"
lock_trust_level: "ล็อกระดับความไว้ใจ"
unlock_trust_level: "ปลดล็อกระดับความไว้ใจ"
suspended_count: "ระงับการใช้งาน"
@@ -3038,19 +3036,17 @@ th:
delete_confirm: "คุณแน่ใจหรือว่าจะลบฟิวส์ของผู้ใช้นี้?"
options: "ตัวเลือก"
required:
- title: "ต้องการเมื่อลงทะเบียน?"
+ title: "ต้องการเมื่อลงทะเบียน"
enabled: "ต้องการ"
disabled: "ไม่ต้องการ"
editable:
- title: "แก้ไขหลังลงทะเบียนได้ใช่ไหม"
enabled: "แก้ไขได้"
disabled: "แก้ไขไม่ได้"
show_on_profile:
- title: "แสดงในข้อมูลสาธารณะ?"
+ title: "แสดงในข้อมูลสาธารณะ"
enabled: "แสดงแสดงในข้อมูลส่วนตัว"
disabled: "ไม่แสดงในข้อมูลส่วนตัว"
show_on_user_card:
- title: "แสดงบนการ์ดผู้ใช้?"
enabled: "แสดงบนการ์ดผู้ใช้"
disabled: "ไม่แสดงบนการ์ดผู้ใช้"
field_types:
diff --git a/config/locales/client.tr_TR.yml b/config/locales/client.tr_TR.yml
index b9e8fb78a4..8979ce87f2 100644
--- a/config/locales/client.tr_TR.yml
+++ b/config/locales/client.tr_TR.yml
@@ -1089,9 +1089,6 @@ tr_TR:
enable: "Kenar çubuğunu etkinleştir"
options: "Seçenekler"
navigation_section: "Navigasyon"
- list_destination_instruction: "Kenar çubuğunda yeni veya okunmamış konuların bulunduğu bir konu listesi bağlantısını tıkladığımda beni şuraya götür:"
- list_destination_default: "Varsayılan"
- list_destination_unread_new: "Yeni/Okunmamış"
change: "değiştir"
featured_topic: "Öne Çıkan Konu"
moderator: "%{user} moderatördür"
@@ -3796,7 +3793,7 @@ tr_TR:
title: "gözden geçirmeler"
welcome_topic_banner:
title: "Hoş Geldiniz konulu gönderinizi oluşturun"
- description: 'Hoş geldiniz başlığınız yeni üyelerin okuyacağı ilk şeydir. Bunu "asansör konuşmanız" veya "misyon beyanınız" olarak düşünün. Onlara bu topluluğun kimler için olduğunu, burada neler bulabileceklerini ve ilk olarak ne yapmalarını istediğinizi söyleyin.'
+ description: "Hoş geldiniz başlığınız yeni üyelerin okuyacağı ilk şeydir. Bunu \"asansör konuşmanız\" veya \"misyon beyanınız\" olarak düşünün. Onlara bu topluluğun kimler için olduğunu, burada neler bulabileceklerini ve ilk olarak ne yapmalarını istediğinizi söyleyin."
button_title: "Düzenlemeye Başla"
until: "Şuna kadar:"
admin_js:
@@ -4062,7 +4059,6 @@ tr_TR:
go_back: "Listeye geri dön"
payload_url: "URL Veri yükü"
payload_url_placeholder: "https://ornek.com/gonderial"
- warn_local_payload_url: "Öyle görünüyor ki, web kancasını yerel bir URL'ye ayarlamaya çalışıyorsunuz. Yerel bir adrese iletilen olay bilgileri beklenmedik durumlara neden olabilir. Sürdürmek istiyor musunuz?"
secret_invalid: "Gizli alanda boş karakter olamaz."
secret_too_short: "Gizli alan en az 12 karakter olmalı."
secret_placeholder: "İmza oluşturmak için isteğe bağlı metin"
@@ -4345,7 +4341,6 @@ tr_TR:
is_private: "Tema özel bir git veri havuzunda"
remote_branch: "Şube adı (isteğe bağlı)"
public_key: "Repo'ya aşağıdaki genel anahtar erişimini ver:"
- public_key_note: "Yukarıya geçerli bir özel depo URL'i girildiğinde, bir SSH anahtarı oluşturulacak ve burada görüntülenecektir."
install: "Yükle"
installed: "Yüklendi"
install_popular: "Gözde"
@@ -5020,7 +5015,6 @@ tr_TR:
trust_level_2_users: "Güven Düzeyi 2 Olan Kullanıcılar"
trust_level_3_requirements: "Güven Düzeyi 3 Gereksinimleri"
trust_level_locked_tip: "güven düzeyi kilitlendi, sistem kullanıcının düzeyini yükseltmeyecek veya düşürmeyecek"
- trust_level_unlocked_tip: "güven düzeyi kilitli değil, sistem kullanıcının düzeyini yükseltebilir veya düşürebilir"
lock_trust_level: "Güven Düzeyini Kilitle"
unlock_trust_level: "Güven Düzeyi Kilidini Aç"
silenced_count: "Susturuldu"
@@ -5080,23 +5074,18 @@ tr_TR:
delete_confirm: "Bu kullanıcı alanını silmek istediğine emin misin?"
options: "Seçenekler"
required:
- title: "Kayıt olurken zorunlu mu?"
enabled: "zorunlu"
disabled: "zorunlu değil"
editable:
- title: "Kayıt sonrası düzenlenebilir mi?"
enabled: "düzenlenebilir"
disabled: "düzenlenemez"
show_on_profile:
- title: "Herkese açık profilde gösterilsin mi?"
enabled: "profilde gösteriliyor"
disabled: "profilde gösterilmiyor"
show_on_user_card:
- title: "Kullanıcı profilinde gösterilsin mi?"
enabled: "kullanıcı profilinde gösterildi"
disabled: "kullanıcı profilinde gösterilmiyor"
searchable:
- title: "Aranabilir mi?"
enabled: "aranabilir"
disabled: "aranamaz"
field_types:
diff --git a/config/locales/client.uk.yml b/config/locales/client.uk.yml
index 6a9e87ef01..05c5d2a601 100644
--- a/config/locales/client.uk.yml
+++ b/config/locales/client.uk.yml
@@ -1217,7 +1217,6 @@ uk:
tags_section: "Розділ Теги"
tags_section_instruction: "Вибрані теги відображатимуться в розділі тегів бічної панелі."
navigation_section: "Навігація"
- list_destination_default: "За замовчуванням"
change: "змінити"
featured_topic: "Закріплені теми"
moderator: "%{user} є модератором"
@@ -4512,7 +4511,6 @@ uk:
go_back: "Повернутися до списку"
payload_url: "Посилання для відправки"
payload_url_placeholder: "https://example.com/postreceive"
- warn_local_payload_url: "Здається, ви намагаєтеся налаштувати web-хук для локального посилання. Подія, доставлена на локальну адресу, може призвести до побічних ефектів або непередбачуваних дій. Продовжити?"
secret_invalid: "Ключ не повинен містити порожніх символів."
secret_too_short: "Ключ повинен бути не менше 12 символів."
secret_placeholder: "Додатковий рядок, використовується для створення підпису"
@@ -4805,7 +4803,6 @@ uk:
last_attempt: "Процес встановлення не завершено, остання спроба:"
remote_branch: "Назва гілки (не обов’язково)"
public_key: "Надайте доступ до сховища з наступним відкритим ключем:"
- public_key_note: "Після введення коректної адреси приватного сховища вище, тут буде згенеровано і показано SSH ключ."
install: "Install"
installed: "Встановлена"
install_popular: "Популярні"
@@ -5493,7 +5490,6 @@ uk:
trust_level_2_users: "Користувачі з Рівнем довіри 2"
trust_level_3_requirements: "Вимоги для Рівня довіри 3"
trust_level_locked_tip: "рівень довіри заморожений, система не зможе за потреби розжалувати або просунути користувача"
- trust_level_unlocked_tip: "рівень довіри розморожений, система зможе за потреби розжалувати або просунути користувача"
lock_trust_level: "Заморозити рівень довіри"
unlock_trust_level: "Розморозити рівень довіри"
silenced_count: "Відключений"
@@ -5555,23 +5551,23 @@ uk:
delete_confirm: "Ви впевнені, що хочете видалити це поле?"
options: "Налаштування"
required:
- title: "Обов’язкове під час реєстрації?"
+ title: "Обов’язкове під час реєстрації"
enabled: "Обов’язкове"
disabled: "Необов’язкове"
editable:
- title: "Редаговане після реєстрації?"
+ title: "Редаговане після реєстрації"
enabled: "Редаговане"
disabled: "Нередаговане"
show_on_profile:
- title: "Показувати в публічному профілі?"
+ title: "Показувати в публічному профілі"
enabled: "Показувати в профілі"
disabled: "Не показувати в профілі"
show_on_user_card:
- title: "Показувати в картці користувача?"
+ title: "Показувати в картці користувача"
enabled: "показується в картці користувача"
disabled: "Не показувати в картці користувача"
searchable:
- title: "Включати при пошуку по сайту?"
+ title: "Включати при пошуку по сайту"
enabled: "для пошуку"
disabled: "не для пошуку"
field_types:
diff --git a/config/locales/client.ur.yml b/config/locales/client.ur.yml
index bfe2ddb846..6cd2eec50a 100644
--- a/config/locales/client.ur.yml
+++ b/config/locales/client.ur.yml
@@ -1071,7 +1071,6 @@ ur:
experimental_sidebar:
options: "اختیارات"
navigation_section: "نیویگیشن"
- list_destination_default: "ڈِیفالٹ"
change: "بدلیں"
featured_topic: "نمایاں موضوع"
moderator: "%{user} ایک ماڈریٹر ہے"
@@ -3951,7 +3950,6 @@ ur:
go_back: "واپس فہرست پر"
payload_url: "پَیلوڈ یو.آر.ایل."
payload_url_placeholder: "https://example.com/postreceive"
- warn_local_payload_url: "ایسا لگتا ہے کہ آپ ویب ہک کو مقامی یو آر ایل پر سیٹ کرنے کی کوشش کر رہے ہیں۔ مقامی پتے پر پہنچایا گیا واقعہ ضمنی اثرات یا غیر متوقع طرز عمل کا سبب بن سکتا ہے۔ جاری رہے؟"
secret_invalid: "سیکرٹ میں کوئی بھی خالی حروف نہ ہونا ضروری ہے۔"
secret_too_short: "سیکرٹس کا کم از کم 12 حروف پر مشتمل ہونا ضروری ہے۔"
secret_placeholder: "ایک اختیاری سٹرنگ، سِگنَیچر تخلیق کرنے کیلئے استعمال کیا جاتا ہے"
@@ -4238,7 +4236,6 @@ ur:
is_private: "تھِیم ایک ذاتی گِٹ رِیپَوزِٹَری میں ہے"
remote_branch: "برانچ کا نام (اختیاری)"
public_key: "رِیپَوزِٹَری کو درج ذیل پبلک کلید ایکسَیس فراہم کریں:"
- public_key_note: "اوپر ایک درست پرائیویٹ ریپوزٹری یو آر ایل داخل کرنے کے بعد، ایک SSH کی یہاں تیار اور ڈسپلے کی جائے گی۔"
install: "انسٹال"
installed: "انسٹال کیا ہوا"
install_popular: "مقبول"
@@ -4905,7 +4902,6 @@ ur:
trust_level_2_users: "ٹرسٹ لَیول 2 کے صارفین"
trust_level_3_requirements: "ٹرسٹ لَیول 3 کے تقاضے"
trust_level_locked_tip: "ٹرسٹ لَیول لاک ہے، سِسٹم صارف کا لَیول زیادہ یا کم نہیں کرے گا"
- trust_level_unlocked_tip: "ٹرسٹ لَیول لاک نہیں ہے، سِسٹم صارف کا لَیول زیادہ یا کم کرسکتا ہے"
lock_trust_level: "ٹرسٹ لَیول لاک کریں"
unlock_trust_level: "ٹرسٹ لَیول کا لاک ختم کریں"
silenced_count: "خاموش کیو ہوئے"
@@ -4965,23 +4961,18 @@ ur:
delete_confirm: "کیا آپ واقعی یہ صارف سے بھرے جانے والا خانا حذف کرنا چاہتے ہیں؟"
options: "اختیارات"
required:
- title: "سائن اَپ کے وقت درکار ہے؟"
enabled: "درکار ہے"
disabled: "درکار نہیں ہے"
editable:
- title: "سائن اَپ کے بعد قابل ترمیم؟"
enabled: "قابلِ ترمیم"
disabled: "ناقابلِ ترمیم"
show_on_profile:
- title: "پبلک پروفائل پر دکھائیں؟"
enabled: "پروفائل پر دکھائیں؟"
disabled: "پروفائل پر نہیں دکھایا؟"
show_on_user_card:
- title: "صارف کارڈ پر دکھائیں؟"
enabled: "صارف کارڈ پر دکھایا گیا"
disabled: "صارف کارڈ پر نہیں دکھایا گیا"
searchable:
- title: "قابل تلاش؟"
enabled: "قابل تلاش؟"
disabled: "قابل تلاش نہیں"
field_types:
diff --git a/config/locales/client.vi.yml b/config/locales/client.vi.yml
index 8ace446bba..cfb1824c8d 100644
--- a/config/locales/client.vi.yml
+++ b/config/locales/client.vi.yml
@@ -1041,7 +1041,6 @@ vi:
tags_section: "Phần thẻ"
tags_section_instruction: "Các thẻ đã chọn sẽ được hiển thị trong phần thẻ của Thanh bên."
navigation_section: "Điều hướng"
- list_destination_default: "Mặc định"
change: "thay đổi"
featured_topic: "Chủ đề nổi bật"
moderator: "%{user} trong ban quản trị"
@@ -3922,7 +3921,6 @@ vi:
go_back: "Quay lại danh sách"
payload_url: "Payload URL"
payload_url_placeholder: "https://example.com/postreceive"
- warn_local_payload_url: "Có vẻ như bạn đang cố gắng thiết lập webhook thành một url cục bộ. Sự kiện được gửi đến một địa chỉ cục bộ có thể gây ra tác dụng phụ hoặc các hành vi không mong muốn. Tiếp tục?"
secret_invalid: "Bí mật không được có bất kỳ ký tự trống nào."
secret_too_short: "Bí mật phải có ít nhất 12 ký tự."
secret_placeholder: "Một chuỗi tùy chọn, được sử dụng để tạo chữ ký"
@@ -4207,7 +4205,6 @@ vi:
is_private: "Theme nằm trong kho git riêng"
remote_branch: "Tên chi nhánh (không bắt buộc)"
public_key: "Cấp quyền truy cập khóa công khai sau vào repo:"
- public_key_note: "Sau khi nhập URL kho lưu trữ riêng hợp lệ ở trên, khóa SSH sẽ được tạo và hiển thị ở đây."
install: "Cài đặt"
installed: "Đã cài đặt"
install_popular: "Phổ biến"
@@ -4827,7 +4824,6 @@ vi:
trust_level_2_users: "Độ tin cậy tài khoản mức 2"
trust_level_3_requirements: "Độ tin cậy bắt buộc mức 3"
trust_level_locked_tip: "mức độ tin cậy đang khóa, hệ thống sẽ không thể thăng hoặc giáng chức người dùng"
- trust_level_unlocked_tip: "độ tin cậy đang được mở, hệ thống có thể thăng hoặc giáng chức người dùng"
lock_trust_level: "Khóa Cấp độ Tin tưởng"
unlock_trust_level: "Mở khóa độ tin cậy"
silenced_count: "Im lặng"
@@ -4883,19 +4879,18 @@ vi:
delete_confirm: "Bạn muốn xóa trường thành viên?"
options: "Lựa chọn"
required:
- title: "Bắt buộc lúc đăng ký?"
+ title: "Bắt buộc lúc đăng ký"
enabled: "bắt buộc"
disabled: "không bắt buộc"
editable:
- title: "Có thể chỉnh sửa sau khi đăng ký?"
+ title: "Có thể chỉnh sửa sau khi đăng ký"
enabled: "có thể chỉnh sửa"
disabled: "không thể chỉnh sửa"
show_on_profile:
- title: "Hiển thị trong hồ sơ công khai"
enabled: "hiển thị trong hồ sơ"
disabled: "không hiển thị trong hồ sơ"
show_on_user_card:
- title: "Hiện trên thẻ người dùng?"
+ title: "Hiện trên thẻ người dùng"
enabled: "hiển trên thẻ người dùng"
disabled: "không hiện trên thẻ người dùng"
searchable:
diff --git a/config/locales/client.zh_CN.yml b/config/locales/client.zh_CN.yml
index 29699e65d8..eef7f7a310 100644
--- a/config/locales/client.zh_CN.yml
+++ b/config/locales/client.zh_CN.yml
@@ -915,7 +915,7 @@ zh_CN:
user_fields:
none: "(选择一个选项)"
required: '请为“%{name}”输入一个值。'
- same_as_password: '您的密码不应重复出现在其他字段中。'
+ same_as_password: "您的密码不应重复出现在其他字段中。"
user:
said: "%{username}:"
profile: "个人资料"
@@ -927,7 +927,7 @@ zh_CN:
success: "下载已开始,完成后将通过消息通知您。"
rate_limit_error: "帖子每天只能下载一次,请明天再试。"
new_private_message: "新消息"
- private_message: "消息"
+ private_message: "私信"
private_messages: "消息"
user_notifications:
filters:
@@ -3712,7 +3712,7 @@ zh_CN:
pending_count: "%{count} 待处理"
welcome_topic_banner:
title: "创建您的欢迎话题"
- description: '您的欢迎话题是新成员首先会阅读的内容。把它想象成您的“电梯推销”或“使命宣言”。让每个人都知道这个社区是为谁服务的,他们可以在这里找到什么,以及您希望他们首先做什么。'
+ description: "您的欢迎话题是新成员首先会阅读的内容。把它想象成您的“电梯推销”或“使命宣言”。让每个人都知道这个社区是为谁服务的,他们可以在这里找到什么,以及您希望他们首先做什么。"
button_title: "开始编辑"
until: "直到:"
admin_js:
@@ -3984,7 +3984,6 @@ zh_CN:
go_back: "返回列表"
payload_url: "有效负载 URL"
payload_url_placeholder: "https://example.com/postreceive"
- warn_local_payload_url: "您似乎正在尝试将网络钩子设置为本地 URL。传递到本地地址的事件可能会导致副作用或意外行为。继续吗?"
secret_invalid: "密钥不得包含任何空白字符。"
secret_too_short: "密钥应至少为 12 个字符。"
secret_placeholder: "可选字符串,用于生成签名"
@@ -4271,7 +4270,6 @@ zh_CN:
last_attempt: "安装过程未完成,最后一次尝试:"
remote_branch: "分支名称(可选)"
public_key: "授予以下公钥访问仓库的权限:"
- public_key_note: "在上面输入有效的私有仓库 URL 后,将生成 SSH 密钥并在此处显示。"
install: "安装"
installed: "已安装"
install_popular: "热门"
@@ -4945,7 +4943,6 @@ zh_CN:
trust_level_2_users: "信任级别 2 用户"
trust_level_3_requirements: "信任级别 3 要求"
trust_level_locked_tip: "信任级别已被锁定,系统将不会升降用户的信任级别"
- trust_level_unlocked_tip: "信任级别已被解锁,系统可能会升降用户的信任级别"
lock_trust_level: "锁定信任级别"
unlock_trust_level: "解锁信任级别"
silenced_count: "被禁言"
@@ -5004,23 +5001,18 @@ zh_CN:
delete_confirm: "确定要删除该用户字段吗?"
options: "选项"
required:
- title: "注册时需要?"
enabled: "必选"
disabled: "非必选"
editable:
- title: "注册后可以编辑?"
enabled: "可编辑"
disabled: "不可编辑"
show_on_profile:
- title: "在公开个人资料中显示?"
enabled: "在个人资料中显示"
disabled: "不在个人资料中显示"
show_on_user_card:
- title: "在用户卡片上显示?"
enabled: "在用户卡片上显示"
disabled: "不在用户卡片上显示"
searchable:
- title: "可搜索?"
enabled: "可搜索"
disabled: "不可搜索"
field_types:
diff --git a/config/locales/client.zh_TW.yml b/config/locales/client.zh_TW.yml
index 28d94f690d..24fe06ac6c 100644
--- a/config/locales/client.zh_TW.yml
+++ b/config/locales/client.zh_TW.yml
@@ -202,6 +202,7 @@ zh_TW:
delete: "刪除"
generic_error: "抱歉,發生錯誤。"
generic_error_with_reason: "發生錯誤: %{error}"
+ multiple_errors: "發生多個錯誤: %{errors}"
sign_up: "註冊"
log_in: "登入"
age: "已建立"
@@ -963,9 +964,9 @@ zh_TW:
tags_section: "選擇標籤"
tags_section_instruction: "選定的標籤將顯示在側選單的標籤段落中。"
navigation_section: "導覽"
- list_destination_instruction: "當我點擊側選單中包含新主題或未讀主題的列表連結時,帶我前往"
- list_destination_default: "預設"
- list_destination_unread_new: "新文章/未讀"
+ list_destination_instruction: "當側邊欄中有新內容時..."
+ list_destination_default: "使用預設連結並顯示新項目的徽章"
+ list_destination_unread_new: "連結至未讀/新的並顯示新項目的數量"
change: "修改"
featured_topic: "特色主題"
moderator: "%{user} 是板主"
@@ -1538,6 +1539,8 @@ zh_TW:
success: "你的帳號已被建立,且您已經登入了。"
name_label: "姓名"
password_label: "密碼"
+ existing_user_can_redeem: "兌換您對某個主題或群組的邀請。"
+ existing_user_cannot_redeem: "此邀請無法兌換。請找當初邀請您的人來重發一個新的邀請。"
password_reset:
continue: "繼續連接至 %{site_name}"
emoji_set:
@@ -2575,6 +2578,7 @@ zh_TW:
views_lowercase:
other: "觀看"
replies: "回覆"
+ sr_replies: "依回覆排序"
views_long:
other: "這個話題已經被檢視過 %{number} 次"
activity: "活動"
@@ -3869,7 +3873,6 @@ zh_TW:
trust_level_2_users: "信任等級 2 使用者"
trust_level_3_requirements: "信任等級 3 之條件"
trust_level_locked_tip: "信任等級鎖定,系統將不會升級或降級使用者。"
- trust_level_unlocked_tip: "信任等級解除鎖定,系統將會升級或降級使用者。"
lock_trust_level: "鎖住信任等級"
unlock_trust_level: "解鎖信任等級"
silenced_count: "被靜音"
@@ -3921,21 +3924,23 @@ zh_TW:
delete_confirm: "你確定要刪除此使用者欄位 ?"
options: "選項"
required:
- title: "在註冊時必填?"
+ title: "在註冊時必填"
enabled: "必填"
disabled: "非必填"
editable:
- title: "在註冊後可以修改?"
+ title: "註冊後可以修改"
enabled: "可編輯"
disabled: "不可編輯"
show_on_profile:
- title: "顯示在公開的基本資料裡?"
+ title: "顯示在公開的基本資料裡"
enabled: "在基本資料裡顯示"
disabled: "不在基本資料裡顯示"
show_on_user_card:
- title: "在使用者卡片上顯示?"
+ title: "在使用者卡片上顯示"
enabled: "在使用者卡片上顯示"
disabled: "在使用者卡片上隱藏"
+ searchable:
+ title: "可搜索"
field_types:
text: "文字區域"
confirm: "確認"
diff --git a/config/locales/server.ar.yml b/config/locales/server.ar.yml
index 72605365aa..71521d6fe3 100644
--- a/config/locales/server.ar.yml
+++ b/config/locales/server.ar.yml
@@ -71,6 +71,7 @@ ar:
file_too_big: "الملف غير المضغوط كبير جدًا."
unknown_file_type: "يبدو أن الملف الذي حمَّلته ليس سمة Discourse صالحة."
not_allowed_theme: "`%{repo}` غير مُدرَج في قائمة السمات المسموح بها (ضع علامة في مربع الإعداد العام `allowed_theme_repos`)."
+ ssh_key_gone: "لقد انتظرت طويلًا لتثبيت السمة وانتهت صلاحية مفتاح SSH. يُرجى إعادة المحاولة."
errors:
component_no_user_selectable: "لا يمكن أن تكون مكونات السمة قابلة للتحديد بواسطة المستخدم"
component_no_default: "لا يمكن أن تكون مكونات السمة تابعة للسمة الافتراضية"
@@ -131,6 +132,7 @@ ar:
unsubscribe_not_allowed: "يحدث ذلك عندما يكون إلغاء الاشتراك عبر البريد الإلكتروني غير مسموح به لهذا المستخدم."
email_not_allowed: "يحدث عندما لا يكون عنوان البريد الإلكتروني مُدرَجًا في قائمة السماح أو موجودًا في قائمة الحظر."
unrecognized_error: "خطأ غير معروف"
+ secure_uploads_placeholder: "محجوبة: الوسائط الآمنة مفعَّلة في هذا الموقع، قم بزيارة الموضوع أو انقر على \"عرض الوسائط\" لعرض الوسائط المُرفَقة."
view_redacted_media: "عرض الوسائط"
errors: &errors
format: ! "%{attribute} %{message}"
@@ -224,6 +226,7 @@ ar:
page_publishing_requirements: "لا يمكن تفعيل نشر الصفحة في حال تفعيل الوسائط الآمنة."
s3_backup_requires_s3_settings: "لا يمكنك استخدام S3 كمكان للنسخ الاحتياطي ما لم تُدخِل \"%{setting_name}\"."
s3_bucket_reused: "لا يمكنك استخدام الحاوية نفسها لـ \"s3_upload_bucket\" و\"s3_backup_bucket\" معًا. اختر حاويةً أخرى أو استخدم مسارًا مختلفًا لكل حاوية."
+ secure_uploads_requirements: "يجب تفعيل التحميل إلى S3 قبل تفعيل التحميلات الآمنة."
share_quote_facebook_requirements: "يجب عليك ضبط معرِّف تطبيق Facebook لتفعيل مشاركة الاقتباسات على Facebook."
second_factor_cannot_enforce_with_socials: "لا يمكنك فرض المصادقة الثنائية عند تفعيل عمليات تسجيل الدخول بحسابات التواصل الاجتماعي. يجب عليك أولًا إيقاف تسجيل الدخول عبر: %{auth_provider_names}"
second_factor_cannot_be_enforced_with_disabled_local_login: "لا يمكنك فرض المصادقة الثنائية في حال إيقاف عمليات تسجيل الدخول المحلية."
@@ -231,9 +234,10 @@ ar:
local_login_cannot_be_disabled_if_second_factor_enforced: "لا يمكنك إيقاف تسجيل الدخول المحلي في حال فرض المصادقة الثنائية. أوقف المصادقة الثنائية المفروضة قبل إيقاف عمليات تسجيل الدخول المحلية."
cannot_enable_s3_uploads_when_s3_enabled_globally: "لا يمكنك تفعيل تحميلات S3 لأن تحميلات S3 مفعَّلة بشكلٍ عام بالفعل، وقد يتسبب تفعيل مستوى الموقع هذا في حدوث مشكلات خطيرة في التحميلات"
cors_origins_should_not_have_trailing_slash: "يجب عدم إضافة الشرطة المائلة اللاحقة (/) إلى مصادر CORS."
- slow_down_crawler_user_agent_must_be_at_least_3_characters: "يجب أن يكون طول وكلاء المستخدم 3 أحرف على الأقل لتجنب تقييد المستخدمين البشريين بشكل غير صحيح."
- slow_down_crawler_user_agent_cannot_be_popular_browsers: "لا يمكنك إضافة أي من القيم التالية إلى الإعداد: %{values}."
- strip_image_metadata_cannot_be_disabled_if_composer_media_optimization_image_enabled: "لا يمكنك تعطيل البيانات الوصفية للصورة الشريطية إذا مُكّن \"تمكين صورة تحسين وسائط الملحن\". عطّل \"تمكين صورة تحسين وسائط الملحن\" قبل تعطيل البيانات الوصفية للصورة الشريطية."
+ slow_down_crawler_user_agent_must_be_at_least_3_characters: "يجب أن يكون طول وكلاء المستخدم 3 أحرف على الأقل لتجنُّب تقييد المستخدمين البشريين بشكلٍ غير صحيح."
+ slow_down_crawler_user_agent_cannot_be_popular_browsers: "لا يمكنك إضافة أيٍّ من القيم التالية إلى الإعداد: %{values}."
+ strip_image_metadata_cannot_be_disabled_if_composer_media_optimization_image_enabled: "لا يمكنك إيقاف إزالة البيانات الوصفية للصورة إذا كان 'composer media optimization image enabled' مفعلًا. أوقف 'composer media optimization image enabled' قبل إيقاف إزالة البيانات الوصفية للصورة."
+ twitter_summary_large_image_no_svg: "لا يمكن أن تكون صور ملخص Twitter المُستخدَمة في البيانات الوصفية لـ twitter:image بتنسيق svg."
conflicting_google_user_id: 'تم تغيير معرِّف حساب Google لهذا الحساب؛ مطلوب تدخُّل فريق العمل لأسباب أمنية. يُرجى التواصل مع فريق العمل وتوجيهه إلى
https://meta.discourse.org/t/76575'
onebox:
invalid_address: "عذرًا، لم نتمكن من إنشاء معاينة لصفحة الويب هذه لتعذُّر العثور على الخادم \"%{hostname}\". بدلًا من المعاينة، سيظهر رابط فقط في منشورك. :cry:"
@@ -259,8 +263,8 @@ ar:
ملاحظة: قد يلزم إدراج البرامج النصية المضمَّنة لجعة خارجية والتي تم تحميلها بواسطة GTM في قائمة السماح في `content security policy script src`."
enable_escaped_fragments: "ارجع إلى واجهة برمجة تطبيقات Ajax-Crawling من Google إذا لم يتم اكتشاف زاحف ويب. راجع https://developers.google.com/webmasters/ajax-crawling/docs/learn-more"
+ moderators_manage_categories_and_groups: "السماح للمشرفين بإنشاء وإدارة الفئات والمجموعات"
moderators_change_post_ownership: "السماح للمشرفين بتغيير ملكية المنشور"
cors_origins: "الأصول المسموح بها لطلبات الموارد متعددة المصادر (CORS). بجب أن يتضمَّن كل مصدر http:// أو https://. ويجب ضبط متغير البيئة للقيمة DISCOURSE_ENABLE_CORS على True لتفعيل CORS."
use_admin_ip_allowlist: "لا يمكن للمسؤولين تسجيل الدخول إلا إذا كانوا على عنوان IP محدَّد في قائمة عناوين IP الخاضعة للمراقبة (المسؤول > السجلات > عناوين IP الخاضعة للمراقبة)."
@@ -1711,6 +1744,7 @@ ar:
allowed_iframes: "قائمة بادئات نطاقات iframe src التي يمكن أن يسمح بها Discourse بأمان في المنشورات"
allowed_crawler_user_agents: "وكلاء المستخدمين لزاحفات الويب التي يجب السماح لها بالوصول إلى الموقع. تحذير! سيؤدي ضبط هذا الإعداد إلى عدم السماح بجميع الزاحفات غير المُدرَجة هنا!"
blocked_crawler_user_agents: "الكلمة الفريدة غير حساسة لحالة الأحرف في سلسلة وكيل المستخدم والتي تحدِّد زاحفات الويب التي لا ينبغي السماح لها بالوصول إلى الموقع. لا تنطبق إذا تم تعريف قائمة السماح."
+ slow_down_crawler_user_agents: 'وكلاء المستخدم لبرامج زحف الويب التي يجب أن يكون معدَّلها محدودًا كما تم إعداده في "slow down crawler rate". يجب أن تتكون كل قيمة من 3 أحرف على الأقل.'
slow_down_crawler_rate: "إذا تم تحديد slow_down_crawler_user_agents، فسيتم تطبيق هذا المعدَّل على جميع الزاحفات (عدد ثواني التأخير بين الطلبات)"
content_security_policy: "تفعيل Content-Security-Policy"
content_security_policy_report_only: "تفعيل Content-Security-Policy-Report-Only"
@@ -1722,6 +1756,7 @@ ar:
post_menu: "تحديد العناصر التي تظهر في قائمة المنشور وترتيبها. مثال: تعديل|إبلاغ|حذف|مشاركة|إشارة مرجعية|رد"
post_menu_hidden_items: "عناصر القائمة التي سيتم إخفاؤها افتراضيًا في قائمة المنشور ما لم يتم النقر على رمز الثلاث نقاط لتوسيع القائمة."
share_links: "تحديد العناصر التي تظهر في مربع حوار المشاركة وترتيبها"
+ allow_username_in_share_links: "السماح بتضمين أسماء المستخدمين في روابط المشاركة. هذا مفيد لمنح الشارات بناءً على الزوار المميزين."
site_contact_username: "اسم مستخدم صالح لفريق العمل لإرسال جميع الرسائل الآلية منه. سيتم استخدام حساب النظام الافتراضي في حال تركها خالية."
site_contact_group_name: "اسم مجموعة صالح ليتم دعوتها إلى جميع الرسائل الآلية"
send_welcome_message: "إرسال رسالة ترحيبية إلى جميع المستخدمين الجُدد مع دليل البدء السريع"
@@ -1729,6 +1764,7 @@ ar:
send_tl2_promotion_message: "إرسال رسالة ترحيبية بشأن الترقية إلى المستخدمين الجُدد في مستوى الثقة 2"
suppress_reply_directly_below: "عدم عرض عدد الردود القابل للتوسيع على منشور عندما يكون هناك رد واحد فقط أسفل هذا المنشور مباشرةً."
suppress_reply_directly_above: "عدم عرض عبارة \"ردًا على\" القابلة للتوسيع في منشور عندما يكون هناك رد واحد فقط فوق هذا المنشور مباشرةً."
+ remove_full_quote: "إزالة الاقتباس تلقائيًا إذا (أ) ظهر في بداية المنشور، (ب) وكان لمنشور بأكمله، (ج) وكان من المنشور السابق مباشرةً. للمزيد من التفاصيل، راجع إزالة الاقتباسات الكاملة من الردود المباشرة"
suppress_reply_when_quoting: "عدم عرض عبارة \"ردًا على\" القابلة للتوسيع في منشور عند اقتباس المنشور للرد."
max_reply_history: "الحد الأقصى لعدد الردود التي سيتم توسيعها عند توسيع عبارة \"ردًا على\""
topics_per_period_in_top_summary: "عدد الموضوعات الأكثر نشاطًا في الملخص الافتراضي للموضوعات الأكثر نشاطًا."
@@ -1743,10 +1779,13 @@ ar:
enable_badges: "تفعيل نظام الشارات"
max_favorite_badges: "الحد الأقصى لعدد الشارات التي يمكن للمستخدم تحديدها"
enable_whispers: "السماح بالاتصالات الخاصة بين فريق العمل داخل الموضوعات."
+ whispers_allowed_groups: "السماح بالاتصالات الخاصة داخل الموضوعات لأعضاء المجموعات المحدَّدة."
allow_index_in_robots_txt: "حدِّد في ملف robots.txt أن هذا الموقع يسمح لمحركات بحث الويب بفهرسته. وفي حالات استثنائية، يمكنك تجاوز ملف robots.txt بشكلٍ دائم."
blocked_email_domains: "قائمة مفصولة بشرائط عمودية لنطاقات البريد الإلكتروني التي لا يتم السماح للمستخدمين بتسجيل حسابات عليها. مثال: mailinator.com|trashmail.net"
allowed_email_domains: "قائمة مفصولة بشرائط عمودية لنطاقات البريد الإلكتروني التي يجب على المستخدمين تسجيل حسابات عليها. تحذير: لن يتم السماح بالمستخدمين المسجَّلين على نطاقات بريد إلكتروني أخرى بخلاف المذكورة هنا!"
+ normalize_emails: "تحقَّق مما إذا كان البريد الإلكتروني الذي تم تطبيعه فريدًا. يزيل البريد الإلكتروني الذي تم تطبيعه جميع النقاط من اسم المستخدم وكل شيء بين الرمزين + و@."
auto_approve_email_domains: "ستتم الموافقة تلقائيًا على المستخدمين الذين لديهم عناوين بريد إلكتروني من قائمة النطاقات هذه."
+ hide_email_address_taken: "عدم إعلام المستخدمين بوجود حساب بعنوان بريد إلكتروني معيَّن في أثناء التسجيل أو في أثناء عملية \"نسيت كلمة المرور\". طلب البريد الإلكتروني الكامل لطلبات \"نسيت كلمة المرور\"."
log_out_strict: "تسجيل خروج المستخدم من جميع الجلسات على جميع الأجهزة عند تسجيل الخروج"
version_checks: "فحص Discourse Hub للحصول على تحديثات الإصدار وإظهار رسائل الإصدار الجديد على /admin لوحة المعلومات"
new_version_emails: "إرسال رسالة إلكترونية إلى عنوان contact_email عند توفُّر إصدار جديد من Discourse"
@@ -1791,6 +1830,9 @@ ar:
google_oauth2_client_secret: "الرمز السري للعميل لتطبيق Google"
google_oauth2_prompt: "قائمة اختيارية مفصولة بمسافات لقيم السلسلة والتي تحدِّد ما إذا كان خادم التفويض يطالب المستخدم بإعادة المصادقة والموافقة. راجع https://developers.google.com/identity/protocols/OpenIDConnect#prompt لمعرفة القيم الممكنة."
google_oauth2_hd: "نطاق Google Apps Hosted اختياري والذي سيقتصر تسجيل الدخول عليه. راجع https://developers.google.com/identity/protocols/OpenIDConnect#hd-param لمزيد من التفاصيل."
+ google_oauth2_hd_groups: "(تجريبي) استرداد مجموعات Google للمستخدمين على النطاق المستضاف عند المصادقة. يمكن استخدام مجموعات Google التي تم استردادها لمنح عضوية مجموعة Discourse تلقائيًا (انظر إعدادات المجموعة). للمزيد من المعلومات، راجع https://meta.discourse.org/t/226850"
+ google_oauth2_hd_groups_service_account_admin_email: "عنوان بريد إلكتروني ينتمي إلى حساب مسؤول Google Workspace. سيتم استخدامه مع بيانات اعتماد حساب الخدمة لإحضار معلومات المجموعة."
+ google_oauth2_hd_groups_service_account_json: "معلومات مفتاح بتنسيق JSON لحساب الخدمة. سيتم استخدامه لإحضار معلومات المجموعة."
enable_twitter_logins: "تفعيل مصادقة Twitter، يتطلب twitter_consumer_key وtwitter_consumer_secret. راجع إعداد تسجيل الدخول عبر Twitter (والتضمينات الغنية) لمنصة Discourse."
twitter_consumer_key: "مفتاح العميل لمصادقة Twitter، مسجَّل على https://developer.twitter.com/apps"
twitter_consumer_secret: "الرمز السري للعميل لمصادقة Twitter، مسجَّل على https://developer.twitter.com/apps"
@@ -1823,14 +1865,16 @@ ar:
verbose_localization: "عرض نصائح موسَّعة بشأن الترجمة في واجهة المستخدم"
previous_visit_timeout_hours: "مدة الزيارة قبل أن نعتبرها الزيارة \"السابقة\"، بالساعات"
top_topics_formula_log_views_multiplier: "قيمة مُضاعِف (n) مرات عرض السجل في معادلة الموضوعات الأكثر نشاطًا: `log(views_count) * (n) + op_likes_count * 0.5 + LEAST(likes_count / posts_count, 3) + 10 + log(posts_count)`"
- top_topics_formula_first_post_likes_multiplier: "قيمة مُضاعِف الإعجابات على أول منشور (n) في معادلة الموضوعات الأكثر نشاطًا: `log(views_count) * 2 + op_likes_count * (n) + LEAST(likes_count / posts_count, 3) + 10 + log(posts_count)`"
- top_topics_formula_least_likes_per_post_multiplier: "قيمة أقل عدد من الإعجابات لكل مُضاعِف منشورات (n) في معادلة الموضوعات الأكثر نشاطًا: `log(views_count) * 2 + op_likes_count * 0.5 + LEAST(likes_count / posts_count, (n)) + 10 + log(posts_count)`"
+ top_topics_formula_first_post_likes_multiplier: "قيمة مُضاعِف تسجيلات الإعجاب على أول منشور (n) في معادلة الموضوعات الأكثر نشاطًا: `log(views_count) * 2 + op_likes_count * (n) + LEAST(likes_count / posts_count, 3) + 10 + log(posts_count)`"
+ top_topics_formula_least_likes_per_post_multiplier: "قيمة أقل عدد من تسجيلات الإعجاب لكل مُضاعِف منشورات (n) في معادلة الموضوعات الأكثر نشاطًا: `log(views_count) * 2 + op_likes_count * 0.5 + LEAST(likes_count / posts_count, (n)) + 10 + log(posts_count)`"
enable_safe_mode: "السماح للمستخدمين بالدخول إلى الوضع الآمن لتصحيح أخطاء المكوِّنات الإضافية"
+ enable_experimental_sidebar_hamburger: "يسمح بتفعيل الشريط الجانبي التجريبي وقائمة الثلاث شرط المنسدلة."
+ enable_sidebar: "يفعِّل الشريط الجانبي التجريبي."
rate_limit_create_topic: "بعد إنشاء موضوع، يجب على المستخدمين الانتظار (n) ثانية قبل إنشاء موضوع آخر."
rate_limit_create_post: "بعد النشر، يجب على المستخدمين الانتظار (n) ثانية قبل إنشاء منشور آخر."
rate_limit_new_user_create_topic: "بعد إنشاء موضوع، يجب على المستخدمين الجُدد الانتظار (n) ثانية قبل إنشاء موضوع آخر."
rate_limit_new_user_create_post: "بعد النشر، يجب على المستخدمين الجُدد الانتظار (n) ثانية قبل إنشاء منشور آخر."
- max_likes_per_day: "أقصى عدد من الإعجابات لكل مستخدم"
+ max_likes_per_day: "أقصى عدد من تسجيلات الإعجاب لكل مستخدم"
max_flags_per_day: "الحد الأقصى لعدد البلاغات لكل مستخدم في اليوم"
max_bookmarks_per_day: "الحد الأقصى اليومي للإشارات المرجعية لكل مستخدم"
max_edits_per_day: "الحد الأقصى اليومي للتعديلات لكل مستخدم"
@@ -1838,6 +1882,7 @@ ar:
max_personal_messages_per_day: "الحد الأقصى لعدد موضوعات الرسائل الشخصية الجديدة التي يمكن للمستخدم إنشاؤها يوميًا"
max_invites_per_day: "الحد الأقصى اليومي للدعوات التي يمكن للمستخدم إرسالها"
max_topic_invitations_per_day: "الحد الأقصى لعدد الدعوات إلى الموضوعات التي يمكن للمستخدم إرسالها يوميًا"
+ max_topic_invitations_per_minute: "الحد الأقصى لعدد الدعوات إلى الموضوعات التي يمكن للمستخدم إرسالها في الدقيقة"
max_logins_per_ip_per_hour: "الحد الأقصى لعدد عمليات تسجيل الدخول المسموح بها لكل عنوان IP في الساعة"
max_logins_per_ip_per_minute: "الحد الأقصى لعدد عمليات تسجيل الدخول المسموح بها لكل عنوان IP في الدقيقة"
max_post_deletions_per_minute: "الحد الأقصى لعدد المنشورات التي يمكن للمستخدم حذفها في الدقيقة. اضبط القيمة على 0 لإيقاف عمليات حذف المنشورات."
@@ -1868,6 +1913,8 @@ ar:
external_emoji_url: "عنوان URL للخدمة الخارجية لصور الرموز التعبيرية. اتركه فارغًا للإيقاف."
use_site_small_logo_as_system_avatar: "استخدام شعار الموقع الصغير بدلًا من الصورة الرمزية لمستخدم النظام. يتطلب وجود الشعار."
restrict_letter_avatar_colors: "قائمة بقيم الألوان السداسية العشرية المكوَّنة من 6 أرقام لاستخدامها في خلفية الصورة الرمزية لحرف الاسم"
+ enable_listing_suspended_users_on_search: "اسمح للمستخدمين المنتظمين بالعثور على المستخدمين المعلَّقين."
+ selectable_avatars_mode: "اسمح للمستخدمين بتحديد صورة رمزية من قائمة selectable_avatars وقصر عمليات تحميل الصور الرمزية المخصَّصة على مستوى الثقة المحدَّد."
selectable_avatars: "قائمة الصور الرمزية التي يمكن للمستخدمين الاختيار منها"
allow_all_attachments_for_group_messages: "السماح بجميع مرفقات البريد الإلكتروني لرسائل المجموعات"
png_to_jpg_quality: "جودة ملف JPG المحوَّل (1 هي أقل جودة، و99 هي أفضل جودة، و100 للإيقاف)"
@@ -1891,8 +1938,8 @@ ar:
tl2_requires_read_posts: "عدد المنشورات التي يجب على المستخدم قراءتها قبل الترقية إلى مستوى الثقة 2"
tl2_requires_time_spent_mins: "عدد الدقائق التي يجب على المستخدم استغراقها في قراءة المنشورات قبل الترقية إلى مستوى الثقة 2"
tl2_requires_days_visited: "عدد الأيام التي يجب على المستخدم زيارة الموقع فيها قبل الترقية إلى مستوى الثقة 2"
- tl2_requires_likes_received: "عدد الإعجابات التي يجب أن يتلقاها المستخدم قبل الترقية إلى مستوى الثقة 2"
- tl2_requires_likes_given: "عدد الإعجابات التي يجب على المستخدم تسجيلها قبل الترقية إلى مستوى الثقة 2"
+ tl2_requires_likes_received: "عدد تسجيلات الإعجاب التي يجب أن يتلقاها المستخدم قبل الترقية إلى مستوى الثقة 2"
+ tl2_requires_likes_given: "عدد تسجيلات الإعجاب التي يجب على المستخدم تسجيلها قبل الترقية إلى مستوى الثقة 2"
tl2_requires_topic_reply_count: "عدد الموضوعات التي يجب على المستخدم الرد عليها قبل الترقية إلى مستوى الثقة 2"
tl3_time_period: "الفترة الزمنية المطلوبة للوصول إلى مستوى الثقة 3 (بالأيام)"
tl3_requires_days_visited: "الحد الأدنى لعدد الأيام التي يحتاجها المستخدم لزيارة الموقع في آخر (الفترة الزمنية المطلوبة للوصول إلى مستوى الثقة 3) يوم للتأهُّل للترقية إلى مستوى الثقة 3. اضبطها على فترة زمنية أعلى من الفترة الزمنية المطلوبة للوصول إلى مستوى الثقة 3 لإيقاف الترقيات إلى مستوى الثقة 3. (0 أو أعلى)"
@@ -1905,8 +1952,8 @@ ar:
tl3_requires_posts_read_all_time: "الحد الأدنى لإجمالي عدد المنشورات التي يجب على المستخدم قراءتها للتأهل إلى مستوى الثقة 3"
tl3_requires_max_flagged: "يجب ألا يكون المستخدم قد تلقى بلاغات من x مستخدم مختلف على أكثر من x منشور في آخر (الفترة الزمنية المطلوبة للوصول إلى مستوى الثقة 3) يوم للتأهُّل للترقية إلى مستوى الثقة 3، حيث تكون x هي قيمة هذا الإعداد .(0 أو أعلى)"
tl3_promotion_min_duration: "الحد الأدنى لعدد الأيام التي تستمر فيها الترقية إلى مستوى الثقة 3 قبل خفض رتبة المستخدم مرة أخرى إلى مستوى الثقة 2"
- tl3_requires_likes_given: "الحد الأدنى لعدد الإعجابات التي يجب منحها في آخر (الفترة الزمنية المطلوبة للوصول إلى مستوى الثقة 3) للتأهُّل للترقية إلى مستوى الثقة 3"
- tl3_requires_likes_received: "الحد الأدنى لعدد الإعجابات التي يجب تلقيها في آخر (الفترة الزمنية المطلوبة للوصول إلى مستوى الثقة 3) يوم للتأهُّل للترقية إلى مستوى الثقة 3"
+ tl3_requires_likes_given: "الحد الأدنى لعدد تسجيلات الإعجاب التي يجب منحها في آخر (الفترة الزمنية المطلوبة للوصول إلى مستوى الثقة 3) للتأهُّل للترقية إلى مستوى الثقة 3"
+ tl3_requires_likes_received: "الحد الأدنى لعدد تسجيلات الإعجاب التي يجب تلقيها في آخر (الفترة الزمنية المطلوبة للوصول إلى مستوى الثقة 3) يوم للتأهُّل للترقية إلى مستوى الثقة 3"
tl3_links_no_follow: "عدم إزالة rel=nofollow من الروابط المنشورة بواسطة مستخدمين من مستوى الثقة 3"
trusted_users_can_edit_others: "السماح للمستخدمين من مستويات الثقة العالية بتعديل محتوى المستخدمين الآخرين"
min_trust_to_create_topic: "الحد الأدنى لمستوى الثقة المطلوب لإنشاء موضوع جديد"
@@ -1914,6 +1961,7 @@ ar:
min_trust_to_edit_wiki_post: "الحد الأدنى لمستوى الثقة المطلوب لتعديل منشور من نوع Wiki."
min_trust_to_edit_post: "الحد الأدنى لمستوى الثقة المطلوب لتعديل المنشورات."
min_trust_to_allow_self_wiki: "الحد الأدنى لمستوى الثقة المطلوب لتحويل منشور المستخدم إلى Wiki."
+ min_trust_to_send_messages: "تم إيقافه، استخدم إعداد 'personal message enabled groups' بدلًا من ذلك. الحد الأدنى لمستوى الثقة المطلوب لإنشاء رسائل شخصية جديدة."
min_trust_to_send_email_messages: "الحد الأدنى لمستوى الثقة المطلوب لإرسال رسائل خاصة عبر البريد الإلكتروني"
min_trust_to_flag_posts: "الحد الأدنى لمستوى الثقة المطلوب للإبلاغ عن المنشورات"
min_trust_to_post_links: "الحد الأدنى لمستوى الثقة المطلوب لتضمين الروابط في المنشورات"
@@ -1931,6 +1979,9 @@ ar:
max_mentions_per_post: "الحد الأقصى لعدد إشعارات @name التي يمكن لأي شخص استخدامها في المنشور"
max_users_notified_per_group_mention: "الحد الأقصى لعدد المستخدمين الذين قد يتلقون إشعارًا إذا تمت الإشارة إلى المجموعة (لن يتم إرسال الإشعارات إذا تم استيفاء الحد الأقصى)"
enable_mentions: "السماح للمستخدمين بالإشارة إلى مستخدمين آخرين"
+ here_mention: "الاسم المستخدم للإشارة باستخدام الرمز @ للسماح للمستخدمين المتميزين بإعلام ما يصل إلى 'max_here_mentioned' من الأشخاص المشاركين في الموضوع. يجب ألا يكون اسم مستخدم موجودًا."
+ max_here_mentioned: "الحد الأقصى لعدد الأشخاص المُشار إليهم بواسطة @here."
+ min_trust_level_for_here_mention: "الحد الأدنى لمستوى الثقة المطلوب للإشارة @here."
create_thumbnails: "إنشاء صور مصغَّرة وصور مبسَّطة أكبر من أن تلائم المنشور"
email_time_window_mins: "الانتظار (n) دقيقة قبل إرسال أي إشعارات عبر البريد الإلكتروني لمنح المستخدمين فرصة لتعديل منشوراتهم والانتهاء منها"
personal_email_time_window_seconds: "الانتظار (n) ثانية قبل إرسال أي إشعارات عبر البريد الإلكتروني بالرسائل الخاصة لمنح المستخدمين فرصة لتعديل رسائلهم والانتهاء منها"
@@ -1968,19 +2019,20 @@ ar:
history_hours_low: "يتم تمييز مؤشر التعديل بشكلٍ طفيف عند تعديل منشور خلال هذا العدد من الساعات."
history_hours_medium: "يتم تمييز مؤشر التعديل بشكلٍ متوسط عند تعديل منشور خلال هذا العدد من الساعات."
history_hours_high: "يتم تمييز مؤشر التعديل بقوة عند تعديل منشور خلال هذا العدد من الساعات."
- topic_post_like_heat_low: "يتم تمييز حقل عدد المنشورات بشكلٍ طفيف بعد تجاوز معدل الإعجابات:المنشور هذه النسبة."
- topic_post_like_heat_medium: "يتم تمييز حقل عدد المنشورات بشكلٍ متوسط بعد تجاوز معدل الإعجابات:المنشور هذه النسبة."
- topic_post_like_heat_high: "يتم تمييز حقل عدد المنشورات بقوة بعد تجاوز معدل الإعجابات:المنشور هذه النسبة."
+ topic_post_like_heat_low: "يتم تمييز حقل عدد المنشورات بشكلٍ طفيف بعد تجاوز معدل تسجيلات الإعجاب:المنشور هذه النسبة."
+ topic_post_like_heat_medium: "يتم تمييز حقل عدد المنشورات بشكلٍ متوسط بعد تجاوز معدل تسجيلات الإعجاب:المنشور هذه النسبة."
+ topic_post_like_heat_high: "يتم تمييز حقل عدد المنشورات بقوة بعد تجاوز معدل تسجيلات الإعجاب:المنشور هذه النسبة."
faq_url: "إذا كان لديك صفحة أسئلة شائعة مستضافة في مكانٍ آخر وتريد استخدامها، فأدخِل عنوان URL لها كاملًا هنا."
tos_url: "إذا كان لديك مستند شروط خدمة مستضاف في مكانٍ آخر وتريد استخدامه، فأدخِل عنوان URL له كاملًا هنا."
privacy_policy_url: "إذا كان لديك مستند مستضاف في مكان آخر لسياسة الخصوصية وتريد استخدامه، فأدخِل عنوان URL الكامل هنا."
log_anonymizer_details: " تحديد ما إذا كان سيتم الاحتفاظ بتفاصيل المستخدم في السجل بعد إخفاء هويته أم لا. ستحتاج إلى إيقاف تشغيل هذا الإعداد عند الامتثال للقانون العام لحماية البيانات (GDPR)."
newuser_spam_host_threshold: "عدد المرات التي يمكن فيها لمستخدم جديد نشر رابط للمضيف نفسه ضمن منشورات `newuser_spam_host_threshold` قبل اعتبارها غير مرغوب فيها"
allowed_spam_host_domains: "قائمة بالنطاقات المستبعدة من اختبار المضيف غير المرغوب فيه. لن يتم تقييد المستخدمين الجُدد أبدًا من إنشاء منشورات تحتوي على روابط إلى هذه النطاقات."
- staff_like_weight: "الأهمية التي يجب منحها لإعجابات فريق العمل (تساوي أهمية الإعجابات من غير فريق العمل 1)"
+ staff_like_weight: "الأهمية التي يجب منحها لإعجابات فريق العمل (تساوي أهمية تسجيلات الإعجاب من غير فريق العمل 1)"
topic_view_duration_hours: "عد مرة عرض الموضوع الجديد مرة واحدة لكل IP/مستخدم كل N ساعة"
user_profile_view_duration_hours: "عد عرض الملف الشخصي للمستخدم الجديد مرة واحدة لكل IP/مستخدم كل N ساعة"
levenshtein_distance_spammer_emails: "عدد الأحرف المختلفة الذي سيسمح بمطابقة جزئية عند مطابقة الرسائل الإلكترونية غير المرغوب فيها"
+ max_new_accounts_per_registration_ip: "توقَّف عن قبول عمليات الاشتراك الجديدة من عنوان IP هذا إذا كان هناك بالفعل (n) حساب من مستوى الثقة 0 منه (ولم يكن لفريق العمل أو من المستوى الثقة 2 أو أعلى). اضبط القيمة على 0 لإيقاف الحد."
min_ban_entries_for_roll_up: "عند النقر على الزر \"تجميع\"، سيتم إنشاء إدخال حظر جديد في الشبكة الفرعية إذا كان هناك (N) من الإدخالات على الأقل."
max_age_unmatched_emails: "حذف إدخالات البريد الإلكتروني الخاضعة للمراقبة غير المتطابقة بعد (N) يوم"
max_age_unmatched_ips: "حذف إدخالات عناوين IP الخاضعة للمراقبة غير المتطابقة بعد (N) يوم"
@@ -2014,6 +2066,7 @@ ar:
max_emails_per_day_per_user: "الحد الأقصى لعدد الرسائل الإلكترونية التي سيتم إرسالها إلى المستخدمين يوميًا. 0 لإيقاف الحد"
enable_staged_users: "إنشاء مستخدمين مؤقتين تلقائيًا عند معالجة الرسائل الإلكترونية الواردة"
maximum_staged_users_per_email: "الحد الأقصى لعدد المستخدمين المؤقتين الذين تم إنشاؤهم في أثناء معالجة رسالة إلكترونية واردة"
+ maximum_recipients_per_new_group_email: "حجب الرسائل الواردة التي تتضمَّن عددًا كبيرًا من المستلمين."
auto_generated_allowlist: "قائمة عناوين البريد الإلكتروني التي لن يتم التحقُّق منها للمحتوى الذي تم إنشاؤه تلقائيًا. مثال: foo@bar.com|discourse@bar.com"
block_auto_generated_emails: "حظر الرسائل الإلكترونية الواردة التي تم تحديدها على أنها منشأة تلقائيًا."
ignore_by_title: "تجاهل الرسائل الإلكترونية الواردة بناءً على عنوانها"
@@ -2033,6 +2086,7 @@ ar:
raw_email_max_length: "عدد الأحرف التي يجب تخزينها للرسائل الإلكترونية الواردة"
raw_rejected_email_max_length: "عدد الأحرف التي يجب تخزينها للرسائل الإلكترونية الواردة التي تم رفضها"
delete_rejected_email_after_days: "حذف الرسائل الإلكترونية المرفوضة التي مضى عليها أكثر من (n) يوم"
+ require_change_email_confirmation: "مطالبة المستخدمين من خارج طاقم العمل بتأكيد عنوان بريدهم الإلكتروني القديم قبل تغييره. لا ينطبق ذلك على المستخدمين من طاقم العمل؛ لذا فإنهم بحاجة دائمًا إلى تأكيد عنوان بريدهم الإلكتروني القديم."
manual_polling_enabled: "إرسال رسائل إلكترونية فورية باستخدام API لردود البريد الإلكتروني"
pop3_polling_enabled: "استقصاء تلقي ردود البريد الإلكتروني عبر POP3"
pop3_polling_ssl: "استخدام SSL أثناء الاتصال بخادم POP3 (موصى به)"
@@ -2111,11 +2165,14 @@ ar:
enable_mobile_theme: "تستخدم الأجهزة الجوَّالة سمة متوافقة مع الجوَّال، مع إمكانية التبديل إلى الموقع الكامل. يمكنك إيقاف هذا إذا كنت تريد استخدام ورقة أنماط مخصَّصة تستجيب بشكلٍ كامل."
dominating_topic_minimum_percent: "ما النسبة المئوية للمنشورات التي يجب على المستخدم إنشاؤها في الموضوع قبل أن يتم تذكيره بالسيطرة الزائدة على الموضوع."
disable_avatar_education_message: "إيقاف الرسالة التعليمية لتغيير الصورة الرمزية"
+ pm_warn_user_last_seen_months_ago: "تحذير المستخدمين، عند إنشاء رسالة شخصية، عندما يكون المستلم المستهدف لم يظهر منذ أكثر من n من الأشهر."
suppress_uncategorized_badge: "عدم إظهار الشارة للموضوعات غير المصنَّفة في قوائم الموضوعات"
header_dropdown_category_count: "عدد الفئات التي يمكن عرضها في القائمة المنسدلة للرأس"
permalink_normalizations: "تطبيق التعبير العادي التالي قبل مطابقة الروابط الثابتة، على سبيل المثال: سيؤدي استخدام /(topic.*)\\?.*/\\1 إلى إزالة سلاسل الاستعلام من مسارات الموضوع. التنسيق هو regex+string. استخدم \\1 وما إلى ذلك للوصول إلى الالتقاطات."
- global_notice: "عرض بانر عام عاجل وطارئ وغير قابل للتجاهل لجميع الزوار. قم بالتغيير إلى قيمة فارغة لإخفائه (مسموح باستخدام HTML)."
+ global_notice: "عرض بانر عام عاجل وطارئ وغير قابل للتجاهل لجميع الزائرين. قم بالتغيير إلى قيمة فارغة لإخفائه (مسموح باستخدام HTML)."
disable_system_edit_notifications: "إيقاف إشعارات التعديل بواسطة مستخدم النظام عندما يكون \"download_remote_images_to_local\" نشطًا."
+ disable_category_edit_notifications: "إيقاف إشعارات تعديل الفئة في الموضوعات."
+ disable_tags_edit_notifications: "إيقاف إشعارات تعديل الوسوم في الموضوعات."
notification_consolidation_threshold: "عدد إشعارات الإعجاب أو طلبات العضوية المتلقاة قبل دمج الإشعارات في رسالة واحدة. اضبط القيمة على 0 للإيقاف."
likes_notification_consolidation_window_mins: "المدة بالدقائق التي يتم فيها دمج إشعارات الإعجاب في إشعار واحد بمجرد الوصول إلى الحد الأقصى. يمكن إعداد الحد الأقصى عبر `SiteSetting.notification_consolidation_threshold`."
automatically_unpin_topics: "إلغاء تثبيت الموضوعات تلقائيًا عندما يصل المستخدم إلى النهاية"
@@ -2135,9 +2192,11 @@ ar:
display_name_on_posts: "إظهار الاسم الكامل للمستخدم في منشوراته بالإضافة إلى اسم المستخدم الخاص به @username"
show_time_gap_days: "عرض الفجوة الزمنية في الموضوع إذا تم إنشاء منشورين بفارق هذا العدد من الأيام"
short_progress_text_threshold: "بعد أن يتجاوز عدد المنشورات في الموضوع هذا الرقم، سيعرض شريط التقدُّم رقم المنشور الحالي فقط. إذا غيَّرت عرض شريط التقدُّم، فقد تحتاج إلى تغيير هذه القيمة."
+ default_code_lang: "تمييز جملة لغة البرمجة الافتراضية المطبَّق على كتل الرموز البرمجية (auto، nohighlight، ruby، python، إلى آخره). يجب أن تكون القيمة حاضرة أيضًا في إعداد الموقع `highlighted languages`."
warn_reviving_old_topic_age: "سيتم عرض تحذير عندما يبدأ شخص ما في الرد على موضوع يكون فيه الرد الأخير أقدم من هذا العدد من الأيام. يمكنك إيقاف هذا الإعداد عن طريق الضبط على 0."
autohighlight_all_code: "فرض تمييز التعليمات البرمجية على جميع كتل التعليمات البرمجية مسبقة التنسيق حتى في حال عدم تحديد اللغة بشكلٍ صريح."
highlighted_languages: "القواعد المضمَّنة لتمييز البنية. (تحذير: قد يؤثر تضمين عدد كبير جدًا من اللغات على الأداء) راجع: https://highlightjs.org/static/demo للحصول على عرض توضيحي"
+ show_copy_button_on_codeblocks: "إضافة زر إلى كتل الرموز البرمجية لنسخ محتويات الكتل إلى حافظة المستخدم."
embed_any_origin: "السماح بالمحتوى القابل للتضمين بغض النظر عن المصدر. هذا الإعداد مطلوب لتطبيقات الأجهزة الجوَّالة ذات نموذج HTML ثابت."
embed_topics_list: "دعم تضمين HTML لقوائم الموضوعات"
embed_set_canonical_url: "ضبط عنوان URL الأساسي للموضوعات المضمَّنة على عنوان URL للمحتوى المضمَّن"
@@ -2154,6 +2213,12 @@ ar:
delete_merged_stub_topics_after_days: "عدد الأيام التي يجب انتظارها قبل حذف الموضوعات البديلة المدمجة بالكامل تلقائيًا. اضبط القيمة على 0 لعدم حذف الموضوعات البديلة أبدًا."
bootstrap_mode_min_users: "الحد الأدنى لعدد المستخدمين المطلوب لإيقاف وضع التمهيد (اضبط القيمة على 0 للإيقاف)"
prevent_anons_from_downloading_files: "منع المستخدمين المجهولين من تنزيل المرفقات"
+ secure_media: 'تم إيقافه: استخدم الإعداد secure_uploads بدلًا من ذلك، ستتم إزالته في Discourse 3.0.'
+ secure_uploads: 'يقيِّد الوصول إلى كل التحميلات (الصور ومقاطع الفيديو والمقاطع الصوتية والنصوص وملفات PDF وملفات ZIP وغير ذلك). بخلاف ذلك، سيكون الوصول مقيَّدًا فقط لتحميلات الوسائط في الرسائل والفئات الخاصة. تحذير: هذا الإعداد معقَّد ويتطلب فهمًا إداريًا عميقًا. انظر موضوع التحميلات الآمنة على Meta لمعرفة التفاصيل.'
+ secure_media_allow_embed_images_in_emails: "تم إيقافه: استخدام secure_uploads_allow_embed_images_in_emails، ستتم إزالته في Discourse 3.0."
+ secure_uploads_allow_embed_images_in_emails: "يسمح بتضمين الصور الآمنة التي عادةً ما يتم تنقيحها في الرسائل الإلكترونية، إذا كان حجمها أصغر من الإعداد `secure uploads max email embed image size kb`."
+ secure_media_max_email_embed_image_size_kb: "تم إيقافه: استخدام secure_uploads_max_email_embed_image_size_kb، ستتم إزالته في Discourse 3.0."
+ secure_uploads_max_email_embed_image_size_kb: "اقتطاع حجم الصور الآمنة التي سيتم تضمينها في الرسائل الإلكترونية إذا تم تفعيل الإعداد `secure uploads allow embed in emails`. سيكون هذا الإعداد بلا أي تأثير دون تفعيله."
slug_generation_method: "اختيار طريقة إنشاء المسار. سيُنشئ الخيار \"ترميز\" سلسلة ترميز النسبة المئوية، بينما سيوقف الخيار \"لا يوجد\" المسار تمامًا."
enable_emoji: "تفعيل الرموز التعبيرية"
enable_emoji_shortcuts: "سيتم تحويل نصوص الوجوه المبتسمة الشائعة مثل :) p: :( إلى رموز تعبيرية"
@@ -2172,6 +2237,7 @@ ar:
max_allowed_message_recipients: "الحد الأقصى المسموح به من المستلمين في الرسالة"
watched_words_regular_expressions: "الكلمات المُراقَبة هي تعبيرات عادية"
enable_diffhtml_preview: "الميزة التجريبية التي تستخدم diffHTML لمزامنة المعاينة بدلًا من إعادة العرض بالكامل"
+ enable_fast_edit: "يفعِّل تحديد جزء صغير من نص المنشور ليتم تعديله مباشرةً."
old_post_notice_days: "عدد الأيام قبل أن يصبح الإشعار بشأن المنشور قديمًا"
new_user_notice_tl: "الحد الأدنى لمستوى الثقة المطلوب لرؤية الإشعارات بشأن منشور مستخدم جديد"
returning_user_notice_tl: "الحد الأدنى من مستوى الثقة المطلوب لرؤية الإشعارات بشأن منشور مستخدم عائد"
@@ -2180,7 +2246,7 @@ ar:
blur_tl0_flagged_posts_media: "تمويه صور المنشورات التي تم الإبلاغ عنها لإخفاء المحتوى المحتمل أن يكون غير آمن للعمل"
enable_page_publishing: "السماح لفريق العمل بنشر الموضوعات إلى عناوين URL الجديدة بنمطهم الخاص"
show_published_pages_login_required: "يمكن للمستخدمين المجهولين رؤية الصفحات المنشورة، حتى عندما يكون تسجيل الدخول مطلوبًا."
- skip_auto_delete_reply_likes: "تخطي حذف المنشورات التي تحتوي على هذا العدد من الإعجابات أو أكثر عند حذف الردود القديمة تلقائيًا"
+ skip_auto_delete_reply_likes: "تخطي حذف المنشورات التي تحتوي على هذا العدد من تسجيلات الإعجاب أو أكثر عند حذف الردود القديمة تلقائيًا"
default_email_digest_frequency: "عدد المرات التي يتلقى فيها المستخدمون رسائل إلكترونية تلخيصية بشكلٍ افتراضي"
default_include_tl0_in_digests: "تضمين المنشورات من المستخدمين الجُدد في الرسائل الإلكترونية التلخيصية بشكلٍ افتراضي. يمكن للمستخدمين تغيير ذلك في تفضيلاتهم."
default_email_level: "ضبط المستوى الافتراضي لإرسال الإشعارات عبر البريد الإلكتروني للموضوعات العادية"
@@ -2198,7 +2264,7 @@ ar:
default_other_enable_defer: "تفعيل وظيفة تأجيل الموضوع بشكلٍ افتراضي"
default_other_dynamic_favicon: "إظهار عدد الموضوعات الجديدة/المحدَّثة على أيقونة المتصفح بشكلٍ افتراضي"
default_other_skip_new_user_tips: "تخطي نصائح وشارات إعداد المستخدم الجديد"
- default_other_like_notification_frequency: "إرسال إشعار إلى المستخدمين بالإعجابات بشكلٍ افتراضي"
+ default_other_like_notification_frequency: "إرسال إشعار إلى المستخدمين بتسجيلات الإعجاب بشكلٍ افتراضي"
default_topics_automatic_unpin: "إلغاء تثبيت الموضوعات تلقائيًا عندما يصل المستخدم إلى النهاية بشكلٍ افتراضي"
default_categories_watching: "قائمة الفئات المُراقَبة بشكلٍ افتراضي"
default_categories_tracking: "قائمة الفئات التي يتم تتبُّعها بشكلٍ افتراضي"
@@ -2230,6 +2296,7 @@ ar:
tags_sort_alphabetically: "عرض الوسوم بترتيب أبجدي. الإعداد الافتراضي هو العرض بترتيب الأكثر رواجًا."
tags_listed_by_group: "إدراج الوسوم حسب مجموعة الوسوم الموجودة في صفحة الوسوم"
tag_style: "النمط المرئي لشارات الوسوم"
+ pm_tags_allowed_for_groups: "السماح لأعضاء المجموعة (المجموعات) المضمَّنة بوسم أي رسالة شخصية"
min_trust_level_to_tag_topics: "الحد الأدنى لمستوى الثقة المطلوب لوضع وسوم على الموضوعات"
suppress_overlapping_tags_in_list: "عدم عرض الوسم إذا كانت الوسوم تتطابق تمامًا مع الكلمات الموجودة في عناوين الموضوعات"
remove_muted_tags_from_latest: "عدم عرض الموضوعات التي تم وضع وسم الكتم فقط عليها في قائمة الموضوعات الحديثة"
@@ -2243,6 +2310,10 @@ ar:
push_notifications_icon: "رمز الشارة الذي يظهر في ركن الإشعارات. يوصى باستخدام صورة PNG أحادية اللون بمقاس 96 × 96 وخلفية شفافة."
base_font: "الخط الأساسي الذي سيتم استخدامه لمعظم النصوص على الموقع. يمكن تجاوز السمات عبر خاصية CSS المخصَّصة `--font-family`."
heading_font: "الخط الذي سيتم استخدامه للعناوين على الموقع. يمكن تجاوز السمات عبر خاصية CSS المخصَّصة `--heading-font-family`."
+ enable_sitemap: "إنشاء خريطة موقع لموقعك وتضمينها في ملف robots.txt."
+ sitemap_page_size: "عدد عناوين URL المراد تضمينها في كل صفحة من خريطة الموقع. الحد الأقصى: 50.000"
+ enable_user_status: "(تجريبي) السماح للمستخدمين بتحديد رسالة حالة مخصَّصة (رمز تعبيري + وصف)."
+ enable_onboarding_popups: "(تجريبي) تفعيل النوافذ المنبثقة التعليمية التي تصف الميزات الرئيسية للمستخدمين"
short_title: "سيتم استخدام العنوان القصير على الشاشة الرئيسية للمستخدم، أو المشغِّل، أو الأماكن الأخرى التي قد تكون المساحة فيها محدودة. يجب أن يقتصر على 12 حرفًا."
dashboard_hidden_reports: "السماح بإخفاء التقارير المحدَّدة من لوحة المعلومات"
dashboard_visible_tabs: "اختيار علامات التبويب المرئية في لوحة المعلومات"
@@ -2254,10 +2325,18 @@ ar:
share_quote_visibility: "تحديد وقت إظهار أزرار مشاركة الاقتباسات: \"أبدًا\" للمستخدمين المجهولين فقط أو لجميع المستخدمين "
create_revision_on_bulk_topic_moves: "إنشاء مراجعة للمنشورات الأولى عند نقل الموضوعات إلى فئة جديدة بشكلٍ مجمَّع"
allow_changing_staged_user_tracking: "السماح بتغيير تفضيلات إشعارات الفئة والوسم لمستخدم مؤقت بواسطة مستخدم مسؤول."
+ use_email_for_username_and_name_suggestions: "استخدام الجزء الأول من عناوين البريد الإلكتروني للحصول على اقتراحات لاسم المستخدم والاسم. لاحظ أن هذا يسهِّل على الجمهور تخمين عناوين البريد الإلكتروني الكاملة للمستخدمين (لأن نسبة كبيرة من الأشخاص يشاركون خدمات مشتركة مثل `gmail.com`)."
+ use_name_for_username_suggestions: "استخدام الاسم الكامل للمستخدم عند اقتراح أسماء المستخدمين."
+ suggest_weekends_in_date_pickers: "تضمين عطلات نهاية الأسبوع (السبت والأحد) في اقتراحات منتقي التاريخ (يمكنك إيقاف هذا الإعداد إذا كنت تستخدم Discouse في أيام الأسبوع فقط؛ أي من الاثنين إلى الجمعة)."
+ splash_screen: "يعرض شاشة تحميل مؤقتة أثناء تحميل أصول الموقع"
+ default_sidebar_categories: "سيتم عرض الفئات المحدَّدة ضمن قسم فئات الشريط الجانبي بشكلٍ افتراضي."
+ default_sidebar_tags: "سيتم عرض الوسوم المحدَّدة ضمن قسم فئات الشريط الجانبي بشكلٍ افتراضي."
+ enable_new_user_profile_nav_groups: "تجريبي: سيتم عرض قائمة التنقل الخاصة بملف تعريف المستخدم الجديد لمستخدمي المجموعات المحدَّدة"
errors:
invalid_css_color: "لون غير صالح. أدخِل اسم لون أو قيمة سداسية عشرية."
invalid_email: "عنوان البريد الإلكتروني غير صالح."
invalid_username: "لا يوجد مستخدم باسم المستخدم هذا."
+ valid_username: "يوجد مستخدم باسم المستخدم هذا."
invalid_group: "لا توجد مجموعة بهذا الاسم."
invalid_integer_min_max: "يجب أن تكون القيمة بين %{min} و%{max}."
invalid_integer_min: "يجب أن تكون القيمة %{min} أو أكبر."
@@ -2272,6 +2351,7 @@ ar:
invalid_json: "ملف JSON غير صالح."
invalid_reply_by_email_address: "يجب أن تحتوي القيمة على \"%{reply_key}\" وأن تكون مختلفة عن الرسالة الإلكترونية للإشعار."
invalid_alternative_reply_by_email_addresses: "يجب أن تحتوي جميع القيم على \"%{reply_key}\" وأن تكون مختلفة عن الرسالة الإلكترونية للإشعار."
+ invalid_domain_hostname: "يجب ألا يتضمَّن الرمز * أو ؟."
pop3_polling_host_is_empty: "يجب عليك ضبط `pop3 polling host` قبل تفعيل استقصاء POP3."
pop3_polling_username_is_empty: "يجب عليك ضبط `pop3 polling username` قبل تفعيل استقصاء POP3."
pop3_polling_password_is_empty: "يجب عليك ضبط `pop3 polling password` قبل تفعيل استقصاء POP3."
@@ -2279,7 +2359,9 @@ ar:
reply_by_email_address_is_empty: "يجب عليك ضبط `reply by email address` قبل تفعيل الرد عن طريق البريد الإلكتروني."
email_polling_disabled: "يجب عليك ضبط الاستقصاء اليدوي أو من خلال POP3 قبل تفعيل الرد عن طريق البريد الإلكتروني."
user_locale_not_enabled: "يجب عليك تفعيل `allow user locale` أولًا قبل تفعيل هذا الإعداد."
+ personal_message_enabled_groups_invalid: "يجب عليك تحديد مجموعة واحدة على الأقل لهذا الإعداد. إذا كنت لا تريد أن يقوم أي شخص باستثناء فريق العمل بإرسال الرسائل الشخصية، فاختر مجموعة فريق العمل."
invalid_regex: "التعبير العادي غير صالح أو غير مسموح به."
+ invalid_regex_with_message: "يحتوي التعبير العادي '%{regex}' على خطأ: %{message}"
email_editable_enabled: "يجب إيقاف `email editable` قبل تفعيل هذا الإعداد."
staged_users_disabled: "يجب عليك تفعيل `staged users` أولًا قبل تفعيل هذا الإعداد."
reply_by_email_disabled: "يجب عليك تفعيل `reply by email` أولًا قبل تفعيل هذا الإعداد."
@@ -2300,6 +2382,12 @@ ar:
leading_trailing_slash: "يجب ألا يبدأ التعبير العادي بشرطة مائلة وينتهي بها."
unicode_usernames_avatars: "لا تدعم الصور الرمزية الداخلية للنظام أسماء المستخدمين بترميز Unicode."
list_value_count: "يجب أن تحتوي القائمة على قيم %{count} بالضبط."
+ markdown_linkify_tlds: "لا يمكنك تضمين قيمة '*'."
+ google_oauth2_hd_groups: "يجب عليك ضبط جميع إعدادات 'google oauth2 hd' قبل تفعيل هذا الإعداد."
+ search_tokenize_chinese_enabled: "يجب عليك إيقاف `email editable` قبل تفعيل هذا الإعداد."
+ search_tokenize_japanese_enabled: "يجب عليك إيقاف 'search_tokenize_japanese' قبل تفعيل هذا الإعداد."
+ discourse_connect_cannot_be_enabled_if_second_factor_enforced: "لا يمكنك تفعيل DiscourseConnect في حال فرض المصادقة الثنائية."
+ delete_rejected_email_after_days: "لا يمكن ضبط هذا الإعداد على أقل من الإعداد delete_email_logs_after_days أو أكثر من %{max}"
placeholder:
discourse_connect_provider_secrets:
key: "www.example.com"
@@ -2537,6 +2625,15 @@ ar:
second_factor_toggle:
totp: "استخدام تطبيق المصادقة أو مفتاح الأمان بدلًا من ذلك"
backup_code: "استخدام رمز احتياطي بدلًا من ذلك"
+ second_factor_auth:
+ challenge_not_found: "تعذَّر العثور على اختبار المصادقة الثنائية في جلستك الحالية."
+ challenge_expired: "مرَّ وقت طويل منذ تقديم اختبار المصادقة الثنائية ولم يعُد صالحًا. حاول مرة أخرى."
+ challenge_not_completed: "لم تكمل اختبار المصادقة الثنائية لاتخاذ هذا الإجراء. يُرجى إكمال اختبار المصادقة الثنائية وإعادة المحاولة."
+ actions:
+ grant_admin:
+ description: "لتدابير الأمان الإضافية، تحتاج إلى تأكيد المصادقة الثنائية قبل منح %{username} وصول المسؤول."
+ discourse_connect_provider:
+ description: "لقد طلب %{hostname} تأكيد المصادقة الثنائية. ستتم إعادة توجيهك مرة أخرى إلى الموقع بمجرد تأكيد المصادقة الثنائية."
admin:
email:
sent_test: "تم الإرسال!"
@@ -2544,7 +2641,7 @@ ar:
merge_user:
updating_username: "جارٍ تحديث اسم المستخدم..."
changing_post_ownership: "جارٍ تغيير ملكية المنشور..."
- merging_given_daily_likes: "جارٍ دمج الإعجابات اليومية الممنوحة..."
+ merging_given_daily_likes: "جارٍ دمج تسجيلات الإعجاب اليومية الممنوحة..."
merging_post_timings: "جارٍ دمج توقيتات النشر..."
merging_user_visits: "جارٍ دمج زيارات المستخدمين..."
updating_site_settings: "جارٍ تحديث إعدادات الموقع..."
@@ -2706,6 +2803,21 @@ ar:
test_mailer:
title: "رسالة الاختبار"
subject_template: "[%{email_prefix}] اختبار تسليم الرسالة الإلكترونية"
+ text_body_template: |
+ هذه رسالة بريد إلكتروني تجريبية من
+
+ [**%{base_url}**][0]
+
+ نأمل أن تكون قد تلقيت اختبار تسليم البريد الإلكتروني هذا!
+
+ فيما يلي [قائمة تحقق مفيدة للتحقق من تكوين تسليم البريد الإلكتروني][1].
+
+ حظًا سعيدًا،
+
+ أصدقاؤك في [Discourse](https://www.discourse.org)
+
+ [0]: %{base_url}
+ [1]: https://meta.discourse.org/t/email-delivery-configuration-checklist/209839
new_version_mailer:
title: "رسالة الإصدار الجديد"
subject_template: "[%{email_prefix}] إصدار جديد من Discourse، يتوفَّر تحديث"
@@ -2719,6 +2831,11 @@ ar:
inappropriate: "تم الإبلاغ عن منشورك على أنه **غير لائق**: يشعر المجتمع بأنه مسيئ أو ينتهك [إرشادات مجتمعنا](%{base_path}/guidelines)."
spam: "تم الإبلاغ عن منشورك على أنه **غير مرغوب فيه**: يشعر المجتمع بأنه إعلان، أو ذو طبيعة ترويجية بشكلٍ زائد بدلًا من أن يكون مفيدًا أو ذا صلة بالموضوع كما هو متوقَّع."
notify_moderators: "تم الإبلاغ عن منشورك على أنه **يستدعي انتباه المشرفين**: يشعر المجتمع بأن المنشور يتطلب تدخلًا يدويًا بواسطة أحد أعضاء فريق العمل."
+ responder:
+ off_topic: "تم الإبلاغ عن المنشور على أنه **خارج الموضوع**: يشعر المجتمع بأنه غير مناسب للموضوع، كما هو محدَّد حاليًا في العنوان وأول منشور."
+ inappropriate: "تم الإبلاغ عن المنشور على أنه **غير لائق**: يشعر المجتمع بأنه مسيئ أو ينتهك [إرشادات مجتمعنا](%{base_path}/guidelines)."
+ spam: "تم الإبلاغ عن المنشور على أنه **غير مرغوب فيه**: يشعر المجتمع بأنه إعلان، أو ذو طبيعة ترويجية بشكلٍ زائد بدلًا من أن يكون مفيدًا أو ذا صلة بالموضوع كما هو متوقَّع."
+ notify_moderators: "تم الإبلاغ عن المنشور على أنه **يستدعي انتباه المشرفين**: يشعر المجتمع بأن المنشور يتطلب تدخلًا يدويًا بواسطة أحد أعضاء فريق العمل."
flags_dispositions:
agreed: "نشكرك على إعلامنا. نتفق على وجود مشكلة ونبحث في الأمر."
agreed_and_deleted: "نشكرك على إعلامنا. نتفق على وجود مشكلة وقد أزلنا المنشور."
@@ -2733,6 +2850,15 @@ ar:
many: "تم إغلاق هذا الموضوع مؤقتًا لمدة %{count} ساعة على الأقل بسبب وجود عدد كبير من بلاغات المجتمع."
other: "تم إغلاق هذا الموضوع مؤقتًا لمدة %{count} ساعة على الأقل بسبب وجود عدد كبير من بلاغات المجتمع."
system_messages:
+ reviewables_reminder:
+ subject_template: "هناك عناصر في قائمة انتظار المراجعة بحاجة إلى المراجعة"
+ text_body_template:
+ zero: "%{mentions} من العناصر تم إرسالها منذ أكثر من %{count} ساعة. [يُرجى مراجعتها](%{base_url}/review)."
+ one: "%{mentions} من العناصر تم إرسالها منذ أكثر من ساعة واحدة (%{count}). [يُرجى مراجعتها](%{base_url}/review)."
+ two: "%{mentions} من العناصر تم إرسالها منذ أكثر من ساعتين (%{count}). [يُرجى مراجعتها](%{base_url}/review)."
+ few: "%{mentions} من العناصر تم إرسالها منذ أكثر من %{count} ساعات. [يُرجى مراجعتها](%{base_url}/review)."
+ many: "%{mentions} من العناصر تم إرسالها منذ أكثر من %{count} ساعة. [يُرجى مراجعتها](%{base_url}/review)."
+ other: "%{mentions} من العناصر تم إرسالها منذ أكثر من %{count} ساعة. [يُرجى مراجعتها](%{base_url}/review)."
private_topic_title: "الموضوع #%{id}"
contents_hidden: "يُرجى زيارة هذا المنشور لرؤية محتوياته."
post_hidden:
@@ -2812,6 +2938,28 @@ ar:
```
يُرجى مراجعة [إرشادات المجتمع](%{base_url}/guidelines) لمعرفة التفاصيل.
+ flags_agreed_and_post_deleted_for_responders:
+ title: "تمت إزالة الرد من المنشور الذي تم الإبلاغ عنه من قِبل فريق العمل"
+ subject_template: "تمت إزالة الرد من المنشور الذي تم الإبلاغ عنه من قِبل فريق العمل"
+ text_body_template: |
+ مرحبًا،
+
+ هذه رسالة تلقائية من %{site_name} لإعلامك بأنه قد تمت إزالة [المنشور](%{base_url}%{url}) الذي رددت عليه.
+
+ %{flag_reason}
+
+ تم الإبلاغ عن هذا المنشور من قِبل المجتمع وقرَّر أحد أعضاء فريق العمل إزالته.
+
+ ``` markdown
+ %{flagged_post_raw_content}
+ ```
+ الذي رددت عليه
+
+ ``` markdown
+ %{flagged_post_response_raw_content}
+ ```
+
+ يُرجى مراجعة [إرشادات المجتمع](%{base_url}/guidelines) للمزيد من التفاصيل بشأن سبب الإزالة.
usage_tips:
text_body_template: |
للحصول على بعض النصائح السريعة بشأن البدء كمستخدم جديد، [اطَّلع على هذه منشور المدونة](https://blog.discourse.org/2016/12/discourse-new-user-tips-and-tricks/).
@@ -2863,24 +3011,72 @@ ar:
[التفضيلات]: %{user_preferences_url}
tl2_promotion_message:
subject_template: "تهانينا على ترقية مستوى الثقة!"
+ text_body_template: |
+ تمت ترقيتك بمقدار [مستوى ثقة واحد!](https://blog.discourse.org/2018/06/understanding-discourse-trust-levels/)!
+
+ يشير حصولك على مستوى الثقة 2 إلى أنك قد قرأت وشاركت بالدرجة الكافية ليتم اعتبارك عضوًا في هذا المجتمع.
+
+ بصفتك مستخدمًا متمرسًا، فقد تنال إعجابك [هذه القائمة من النصائح المفيدة](https://blog.discourse.org/2016/12/discourse-new-user-tips-and-tricks/).
+
+ ندعوك إلى الاستمرار في المشاركة - نحن نستمتع بوجودك.
backup_succeeded:
title: "نجح النسخ الاحتياطي"
subject_template: "اكتمل النسخ الاحتياطي بنجاح"
+ text_body_template: |
+ تم عمل النسخ الاحتياطي بنجاح.
+
+ انتقل إلى [المسؤول > قسم النسخ الاحتياطي](%{base_url}/admin/backups) لتنزيل النسخة الاحتياطية الجديدة.
+
+ إليك السجل:
+
+ %{logs}
backup_failed:
title: "فشل النسخ الاحتياطي"
subject_template: "فشل النسخ الاحتياطي"
+ text_body_template: |
+ فشل النسخ الاحتياطي.
+
+ إليك السجل:
+
+ %{logs}
restore_succeeded:
title: "نجحت الاستعادة"
subject_template: "اكتملت الاستعادة بنجاح"
+ text_body_template: |
+ تمت الاستعادة بنجاح.
+
+ إليك السجل:
+
+ %{logs}
restore_failed:
title: "فشلت الاستعادة"
subject_template: "فشلت الاستعادة"
+ text_body_template: |
+ فشلت الاستعادة.
+
+ إليك السجل:
+
+ %{logs}
bulk_invite_succeeded:
title: "نجحت الدعوة الجماعية"
subject_template: "تمت معالجة الدعوة الجماعية للمستخدمين بنجاح"
+ text_body_template: |
+ تمت معالجة ملف الدعوة الجماعية للمستخدمين، وتم إرسال %{sent} من الدعوات بالبريد، وتخطي %{skipped}، وظهر %{warnings} من التحذيرات.
+
+ ``` text
+ %{logs}
+ ```
bulk_invite_failed:
title: "فشلت الدعوة الجماعية"
subject_template: "تمت معالجة الدعوة الجماعية للمستخدمين مع وجود أخطاء"
+ text_body_template: |
+ تمت معالجة ملف الدعوة الجماعية للمستخدمين، وإرسال %{sent} من الدعوات بالبريد، وتخطي %{skipped}، وظهر %{warnings} من التحذيرات و%{failed} من الأخطاء.
+
+ إليك السجل:
+
+ ``` text
+ %{logs}
+ ```
user_added_to_group_as_owner:
title: "تمت إضافتك إلى المجموعة كمالك"
subject_template: "تمت إضافتك كمالك إلى المجموعة %{group_name}"
@@ -3093,6 +3289,15 @@ ar:
عذرًا، لكن رسالتك الإلكترونية إلى %{destination} (بعنوان %{former_title}) لم تنجح.
لقد رددت على رسالة إلكترونية تلخيصية، إذا كنت تعتقد أن هذا خطأ، [فتواصل مع أحد أعضاء فريق العمل](%{base_url}/about).
+ email_reject_too_many_recipients:
+ title: "تم رفض الرسالة الإلكترونية بسبب كثيرة عدد المستلمين"
+ subject_template: "[%{email_prefix}] مشكلة في الرسالة الإلكترونية -- عدد المستلمين كبير جدًا"
+ text_body_template: |
+ عذرًا، لكن رسالتك الإلكترونية إلى %{destination} (بعنوان %{former_title}) لم تنجح.
+
+ لقد حاولت مراسلة أكثر من %{max_recipients_count} من الأشخاص عبر البريد الإلكتروني، ووضع نظامنا علامة على رسالتك الإلكترونية على أنها بريد غير مرغوب فيه.
+
+ إذا كنت تعتقد أن هذا خطأ، [فتواصل مع عضو في فريق العمل](%{base_url}/about).
email_error_notification:
title: "إشعار بخطأ في الرسالة الإلكترونية"
subject_template: "مشكلة في الرسالة الإلكترونية [%{email_prefix}] - خطأ في مصادقة POP"
@@ -3251,6 +3456,7 @@ ar:
subject_re: "بخصوص: "
subject_pm: "[PM] "
email_from: "%{user_name} عبر %{site_name}"
+ email_from_without_site: "%{group_name}"
user_notifications:
previous_discussion: "الردود السابقة"
reached_limit:
@@ -3273,6 +3479,13 @@ ar:
reply_above_line: "## يُرجى كتابة ردك فوق هذا الخط. ##"
posted_by: "تم النشر بواسطة %{username} في %{post_date}"
pm_participants: "المشاركون: %{participants}"
+ more_pm_participants:
+ zero: "%{participants} و%{count} آخر"
+ one: "%{participants} وواحد (%{count}) آخر"
+ two: "%{participants} واثنان (%{count}) آخران"
+ few: "%{participants} و%{count} آخرين"
+ many: "%{participants} و%{count} آخر"
+ other: "%{participants} و%{count} آخر"
invited_group_to_private_message_body: |
دعا المستخدم %{username} المجموعة @%{group_name} إلى رسالة
@@ -3429,7 +3642,8 @@ ar:
%{respond_instructions}
user_group_mentioned_pm:
- subject_template: "[%{email_prefix}] [PM] %{topic_title}"
+ title: "تمت الإشارة إلى مجموعة المستخدم في رسالة خاصة"
+ subject_template: "[%{email_prefix}] [رسالة شخصية] %{topic_title}"
text_body_template: |
%{header_instructions}
@@ -3439,7 +3653,8 @@ ar:
%{respond_instructions}
user_group_mentioned_pm_group:
- subject_template: "[%{email_prefix}] [PM] %{topic_title}"
+ title: "تمت الإشارة إلى مجموعة المستخدم في رسالة خاصة"
+ subject_template: "[%{email_prefix}] [رسالة شخصية] %{topic_title}"
text_body_template: |
%{header_instructions}
@@ -3541,7 +3756,7 @@ ar:
new_topics: "الموضوعات الجديدة"
unread_notifications: "الإشعارات غير المقروءة"
unread_high_priority: "الإشعارات عالية الأولوية غير المقروءة"
- liked_received: "الإعجابات المتلقاة"
+ liked_received: "تسجيلات الإعجاب المتلقاة"
new_users: "المستخدمون الجُدد"
popular_topics: "الموضوعات الرائجة"
follow_topic: "تابع هذا الموضوع"
@@ -3707,6 +3922,7 @@ ar:
suspicious_login:
title: "تنبيه بعملية تسجيل دخول جديدة"
subject_template: "[%{site_name}] عملية تسجيل دخول جديدة من %{location}"
+ text_body_template: "مرحبًا،\n\nلاحظنا عملية تسجيل دخول من جهاز أو موقع لا تستخدمه عادةً. هل كان هذا أنت؟\n\n - الموقع: %{location} (%{client_ip}) \n - المتصفح: %{browser}\n - الجهاز: %{device} - %{os}\n\nإذا كان هذا أنت، رائع! لا يوجد شيء آخر عليك القيام به.\n\nإذا لم يكن هذا أنت، يُرجى [مراجعة جلساتك الحالية](%{base_url}/my/preferences/security) والتفكير في تغيير كلمة مرورك.\n"
post_approved:
title: "تمت الموافقة على منشورك"
subject_template: "[%{site_name}] تمت الموافقة على منشورك"
@@ -3748,16 +3964,25 @@ ar:
png_to_jpg_conversion_failure_message: "حدث خطأ عند التحويل من PNG إلى JPG."
optimize_failure_message: "حدث خطأ في أثناء تحسين الصورة التي تم تحميلها."
download_failure: "فشل تنزيل الملف من الموفِّر الخارجي."
+ size_mismatch_failure: "لم يتطابق حجم الملف الذي تم تحميله على S3 مع الحجم المخصَّص للتحميل الخارجي. %{additional_detail}"
+ create_multipart_failure: "فشل إنشاء تحميل متعدد الأجزاء في المتجر الخارجي."
+ abort_multipart_failure: "فشل إنهاء تحميل متعدد الأجزاء في المتجر الخارجي."
+ complete_multipart_failure: "فشل إكمال تحميل متعدد الأجزاء في المتجر الخارجي."
+ external_upload_not_found: "لم يتم العثور على التحميل في المتجر الخارجي. %{additional_detail}"
checksum_mismatch_failure: "المجموع الاختباري للملف الذي حمَّلته غير متطابق. ربما تغيَّرت محتويات الملف عند التحميل. حاول مرة أخرى."
cannot_promote_failure: "يتعذَّر إكمال التحميل، ربما يكون قد اكتمل بالفعل أو فشل مسبقًا."
+ size_zero_failure: "عذرًا، يبدو أنه حدث خطأ ما؛ فحجم الملف الذي تحاول تحميله هو 0 بايت. حاول مرة أخرى."
attachments:
too_large: "عذرًا، الملف الذي تحاول تحميله كبير جدًا (الحجم الأقصى هو %{max_size_kb} ك.ب)."
+ too_large_humanized: "عذرًا، الملف الذي تحاول تحميله كبير جدًا (الحجم الأقصى هو %{max_size})."
images:
too_large: "عذرًا، الصورة التي تحاول تحميلها كبيرة جدًا (الحجم الأقصى هو %{max_size_kb} ك.ب)، يُرجى تغيير حجمها وإعادة المحاولة."
+ too_large_humanized: "عذرًا، الصورة التي تحاول تحميلها كبيرة جدًا (الحجم الأقصى هو %{max_size})، يُرجى تغيير حجمها وإعادة المحاولة."
larger_than_x_megapixels: "عذرًا، الصورة التي تحاول تحميلها كبيرة جدًا (الحد الأقصى للأبعاد هو %{max_image_megapixels} ميغابكسل)، يُرجى تغيير حجمها وإعادة المحاولة."
size_not_found: "عذرًا، لكننا لم نتمكَّن من تحديد حجم الصورة. قد تكون صورتك تالفة؟"
placeholders:
too_large: "(الصورة أكبر من %{max_size_kb} كيلوبايت)"
+ too_large_humanized: "(الصورة أكبر من %{max_size})"
avatar:
missing: "عذرًا، لا يمكننا العثور على أي صورة رمزية مرتبطة بعنوان البريد الإلكتروني هذا. هل يمكنك محاولة تحميلها مرة أخرى؟"
flag_reason:
@@ -3802,6 +4027,12 @@ ar:
dark_rose: "الوردي الداكن"
wcag: "WCAG فاتح"
wcag_theme_name: "WCAG فاتح"
+ dracula: "دراكولا"
+ dracula_theme_name: "دراكولا"
+ solarized_light: "فاتح شمسي"
+ solarized_light_theme_name: "فاتح شمسي"
+ solarized_dark: "داكن شمسي"
+ solarized_dark_theme_name: "داكن شمسي"
wcag_dark: "WCAG داكن"
wcag_dark_theme_name: "WCAG داكن"
default_theme_name: "افتراضي"
@@ -3822,7 +4053,7 @@ ar:
عدِّل أول منشور في هذا الموضوع لتغيير محتويات الصفحة %{page_name}.
guidelines_topic:
title: "الأسئلة الشائعة/الإرشادات"
- body: " ## [هذا مكان متحضر للنقاش العام](#civilized)\n\nيُرجى التعامل مع منتدى المناقشة هذا بالاحترام نفسه الذي تتعامل به في الحديقة العامة. نحن أيضًا مورد مجتمعي مشترك — مكان لتبادل المهارات والمعارف والاهتمامات من خلال المحادثة المستمرة.\n\nهذه ليست قواعد صارمة وسريعة. إنها مبادئ توجيهية لمساعدة الحكم الإنساني لمجتمعنا والحفاظ على هذا المكان لطيفًا وودودًا للخطاب العام المتحضر.\n\n\n\n## [تحسين المناقشة](#improve)\n\nساعدنا في جعل هذا مكانًا رائعًا للمناقشة من خلال إضافة شيء إيجابي دائمًا إلى المناقشة، مهما كان صغيرًا. إذا لم تكن متأكدًا من أن منشورك يضيف إلى المحادثة، ففكر فيما تريد قوله وحاول مرة أخرى لاحقًا.\n\nتتمثل إحدى طرق تحسين المناقشة في استكشاف المحادثات الجارية بالفعل. استغرق بعض الوقت في تصفُّح الموضوعات هنا قبل الرد أو البدء في موضوع خاص بك، وستكون لديك فرصة أفضل لمقابلة الآخرين الذين يشاركونك اهتماماتك.\n\nالموضوعات التي تتم مناقشتها هنا تهمنا، ونريدك أن تتصرف كما لو كانت تهمك أيضًا. احترم الموضوعات والأشخاص الذين يناقشونها، حتى لو كنت لا توافق على بعضها.\n\n\n\n## [كُن لطيفًا، حتى عندما لا توافق](#agreeable)\n\nقد ترغب في الرد بمخالفة الرأي. هذا جيد. لكن تذكر أن \"تنتقد الأفكار وليس الأشخاص\". يُرجى تجنُّب:\n\n* التنابز بالألقاب\n* الهجوم الشخصي\n* الرد على نبرة المنشور بدلًا من محتواه الفعلي\n* مخالفة الرأي المتوقعة\n\nبدلًا من ذلك، قدِّم رؤًى عميقة الفكر تعمل على تحسين المحادثة.\n\n\n\n## [مشاركاتك مهمة](#participate)\n\nتحدِّد المحادثات التي لدينا هنا المناخ العام لكل وافد جديد. ساعدنا في التأثير على مستقبل هذا المجتمع من خلال اختيار المشاركة في المناقشات التي تجعل هذا المنتدى مكانًا مثيرًا للاهتمام — وتجنَّب أولئك الذين لا يفعلون ذلك.\n\nيوفِّر Discourse الأدوات التي تتيح للمجتمع تحديد أفضل (وأسوأ) المساهمات بشكلٍ جماعي: الإشارات المرجعية والإعجابات والبلاغات والردود والتعديلات والمراقبة والكتم وما إلى ذلك. استخدم هذه الأدوات لتحسين تجربتك الخاصة وتجربة الآخرين أيضًا.\n\nلنترك مجتمعنا أفضل مما وجدناه.\n\n\n\n## [إذا رأيت مشكلة، فأبلغ عنها](#flag-problems)\n\nيتمتَّع المشرفون بسلطة خاصة؛ فهم مسؤولون عن هذا المنتدى. وكذلك أنت. فبمساعدتك، يمكن أن يصبح المشرفون ميسرين للمجتمع، وليس مجرد عمال نظافة أو شرطة.\n\nعندما ترى سلوكًا سيئًا، لا ترد. يشجع الرد على السلوك السيئ من خلال الاعتراف به ويستهلك طاقتك ويضيع وقت الجميع. _أبلغ عنه فحسب_. إذا تراكم عدد كافٍ من البلاغات، فسيتم اتخاذ الإجراء، إما تلقائيًا أو عن طريق تدخل المشرف.\n\nللحفاظ على مجتمعنا، يحتفظ المشرفون بالحق في إزالة أي محتوى وأي حساب مستخدم لأي سبب في أي وقت. لا يستعرض المشرفون المنشورات الجديدة؛ ولا يتحمَّل المشرفون ومشغِّلو الموقع أي مسؤولية عن أي محتوى ينشره المجتمع.\n\n\n\n## [كُن متحضرًا دائمًا](#be-civil)\n\nلا شيء يفسد المحادثة الصحية مثل الوقاحة:\n\n* كُن متحضرًا. لا تنشر أي شيء يعتبره أي شخص عاقل مسيئًا أو يحض على الكراهية.\n* حافظ على الأدب. لا تنشر أي شيء فاحش أو جنسي صريح.\n* احترم الآخرين. لا تضايق أو تحزن أي شخص، أو تنتحل صفة الأشخاص، أو تكشف عن معلوماتهم الخاصة.\n* احترم منتدانا. لا تنشر محتوى غير مرغوب فيه أو تفسد المنتدى بأي طريقة أخرى.\n\nهذه ليست مصطلحات محدَّدة ذات تعريفات دقيقة — تجنَّب حتى _ظهور_ أي من هذه الأشياء. إذا لم تكن متأكدًا، فاسأل نفسك كيف ستشعر إذا ظهر منشورك في الصفحة الأولى لموقع إخباري رئيسي.\n\nهذا منتدى عام، وتقوم محركات البحث بفهرسة هذه المناقشات. حافظ على اللغة والروابط والصور آمنة للعائلة والأصدقاء.\n\n\n\n## [حافظ على النظام](#keep-tidy)\n\nابذل جهدًا لوضع الأشياء في مكانها الصحيح؛ حتى نتمكن من قضاء المزيد من الوقت في المناقشة وليس التنظيم. وبالتالي:\n\n* لا تبدأ موضوعًا في فئة خاطئة. يُرجى قراءة تعريفات الفئات.\n* لا تنشر الشيء نفسه في موضوعات متعددة.\n* لا تنشر ردودًا دون وجود محتوى.\n* لا تحوِّل موضوعًا عن طريق تغييره في منتصف الطريق.\n* لا توقِّع منشوراتك — معلومات ملفك الشخصي مرفقة بكل منشور. \n\nبدلًا من نشر \"+1\" أو \"أتفق\"، استخدم الزر \"أعجبني\". بدلًا من أخذ موضوع موجود في اتجاه مختلف جذريًا، استخدم \"الرد في موضوع متربط\".\n\n\n\n## [انشر المحتوى الذي أنشأته بنفسك فقط](#stealing)\n\nلا يجوز لك نشر أي محتوى رقمي يخص شخص آخر دون إذن. لا يجوز لك نشر أوصاف أو روابط أو طرق لسرقة الملكية الفكرية لشخص ما (البرامج أو الفيديوهات أو الملفات الصوتية أو الصور) أو لخرق أي قانون آخر.\n\n\n\n## [بدعمٍ منك](#power)\n\nيتم تشغيل هذا الموقع بواسطة [فريق العمل المحلي](%{base_path}/about) والمستخدمين *أمثالك*؛ أي المجتمع. إذا كانت لديك أي أسئلة أخرى بشأن كيفية عمل الأشياء هنا، فافتح موضوعًا جديدًا في [فئة ملاحظات الموقع](%{base_path}/c/site-feedback) ولنناقشها! إذا كانت هناك مشكلة حرجة أو عاجلة لا يمكن معالجتها من خلال موضوع أو علامة وصفية، فتواصل معنا من خلال [صفحة فريق العمل](%{base_path}/about).\n\n\n\n## [شروط الخدمة](#tos)\n\nنعم، الحديث القانوني ممل، ولكن يجب علينا حماية أنفسنا – وبالتبعية حمايتك أنت وبياناتك – ضد الأشخاص غير الودودين. لدينا [شروط الخدمة](%{base_path}/tos) التي تصف سلوكك (وسلوكنا) والحقوق المتعلقة بالمحتوى والخصوصية والقوانين. لاستخدام هذه الخدمة، يجب أن توافق على الالتزام بشروط الخدمة [TOS](%{base_path}/tos).\n"
+ body: " ## [هذا مكان متحضر للنقاش العام](#civilized)\n\nيُرجى التعامل مع منتدى المناقشة هذا بالاحترام نفسه الذي تتعامل به في الحديقة العامة. نحن أيضًا مورد مجتمعي مشترك — مكان لتبادل المهارات والمعارف والاهتمامات من خلال المحادثة المستمرة.\n\nهذه ليست قواعد صارمة وسريعة. إنها مبادئ توجيهية لمساعدة الحكم الإنساني لمجتمعنا والحفاظ على هذا المكان لطيفًا وودودًا للخطاب العام المتحضر.\n\n\n\n## [تحسين المناقشة](#improve)\n\nساعدنا في جعل هذا مكانًا رائعًا للمناقشة من خلال إضافة شيء إيجابي دائمًا إلى المناقشة، مهما كان صغيرًا. إذا لم تكن متأكدًا من أن منشورك يضيف إلى المحادثة، ففكر فيما تريد قوله وحاول مرة أخرى لاحقًا.\n\nتتمثل إحدى طرق تحسين المناقشة في استكشاف المحادثات الجارية بالفعل. استغرق بعض الوقت في تصفُّح الموضوعات هنا قبل الرد أو البدء في موضوع خاص بك، وستكون لديك فرصة أفضل لمقابلة الآخرين الذين يشاركونك اهتماماتك.\n\nالموضوعات التي تتم مناقشتها هنا تهمنا، ونريدك أن تتصرف كما لو كانت تهمك أيضًا. احترم الموضوعات والأشخاص الذين يناقشونها، حتى لو كنت لا توافق على بعضها.\n\n\n\n## [كُن لطيفًا، حتى عندما لا توافق](#agreeable)\n\nقد ترغب في الرد بمخالفة الرأي. هذا جيد. لكن تذكر أن \"تنتقد الأفكار وليس الأشخاص\". يُرجى تجنُّب:\n\n* التنابز بالألقاب\n* الهجوم الشخصي\n* الرد على نبرة المنشور بدلًا من محتواه الفعلي\n* مخالفة الرأي المتوقعة\n\nبدلًا من ذلك، قدِّم رؤًى عميقة الفكر تعمل على تحسين المحادثة.\n\n\n\n## [مشاركاتك مهمة](#participate)\n\nتحدِّد المحادثات التي لدينا هنا المناخ العام لكل وافد جديد. ساعدنا في التأثير على مستقبل هذا المجتمع من خلال اختيار المشاركة في المناقشات التي تجعل هذا المنتدى مكانًا مثيرًا للاهتمام — وتجنَّب أولئك الذين لا يفعلون ذلك.\n\nيوفِّر Discourse الأدوات التي تتيح للمجتمع تحديد أفضل (وأسوأ) المساهمات بشكلٍ جماعي: الإشارات المرجعية وتسجيلات الإعجاب والبلاغات والردود والتعديلات والمراقبة والكتم وما إلى ذلك. استخدم هذه الأدوات لتحسين تجربتك الخاصة وتجربة الآخرين أيضًا.\n\nلنترك مجتمعنا أفضل مما وجدناه.\n\n\n\n## [إذا رأيت مشكلة، فأبلغ عنها](#flag-problems)\n\nيتمتَّع المشرفون بسلطة خاصة؛ فهم مسؤولون عن هذا المنتدى. وكذلك أنت. فبمساعدتك، يمكن أن يصبح المشرفون ميسرين للمجتمع، وليس مجرد عمال نظافة أو شرطة.\n\nعندما ترى سلوكًا سيئًا، لا ترد. يشجع الرد على السلوك السيئ من خلال الاعتراف به ويستهلك طاقتك ويضيع وقت الجميع. _أبلغ عنه فحسب_. إذا تراكم عدد كافٍ من البلاغات، فسيتم اتخاذ الإجراء، إما تلقائيًا أو عن طريق تدخل المشرف.\n\nللحفاظ على مجتمعنا، يحتفظ المشرفون بالحق في إزالة أي محتوى وأي حساب مستخدم لأي سبب في أي وقت. لا يستعرض المشرفون المنشورات الجديدة؛ ولا يتحمَّل المشرفون ومشغِّلو الموقع أي مسؤولية عن أي محتوى ينشره المجتمع.\n\n\n\n## [كُن متحضرًا دائمًا](#be-civil)\n\nلا شيء يفسد المحادثة الصحية مثل الوقاحة:\n\n* كُن متحضرًا. لا تنشر أي شيء يعتبره أي شخص عاقل مسيئًا أو يحض على الكراهية.\n* حافظ على الأدب. لا تنشر أي شيء فاحش أو جنسي صريح.\n* احترم الآخرين. لا تضايق أو تحزن أي شخص، أو تنتحل صفة الأشخاص، أو تكشف عن معلوماتهم الخاصة.\n* احترم منتدانا. لا تنشر محتوى غير مرغوب فيه أو تفسد المنتدى بأي طريقة أخرى.\n\nهذه ليست مصطلحات محدَّدة ذات تعريفات دقيقة — تجنَّب حتى _ظهور_ أي من هذه الأشياء. إذا لم تكن متأكدًا، فاسأل نفسك كيف ستشعر إذا ظهر منشورك في الصفحة الأولى لموقع إخباري رئيسي.\n\nهذا منتدى عام، وتقوم محركات البحث بفهرسة هذه المناقشات. حافظ على اللغة والروابط والصور آمنة للعائلة والأصدقاء.\n\n\n\n## [حافظ على النظام](#keep-tidy)\n\nابذل جهدًا لوضع الأشياء في مكانها الصحيح؛ حتى نتمكن من قضاء المزيد من الوقت في المناقشة وليس التنظيم. وبالتالي:\n\n* لا تبدأ موضوعًا في فئة خاطئة. يُرجى قراءة تعريفات الفئات.\n* لا تنشر الشيء نفسه في موضوعات متعددة.\n* لا تنشر ردودًا دون وجود محتوى.\n* لا تحوِّل موضوعًا عن طريق تغييره في منتصف الطريق.\n* لا توقِّع منشوراتك — معلومات ملفك الشخصي مرفقة بكل منشور. \n\nبدلًا من نشر \"+1\" أو \"أتفق\"، استخدم الزر \"أعجبني\". بدلًا من أخذ موضوع موجود في اتجاه مختلف جذريًا، استخدم \"الرد في موضوع متربط\".\n\n\n\n## [انشر المحتوى الذي أنشأته بنفسك فقط](#stealing)\n\nلا يجوز لك نشر أي محتوى رقمي يخص شخص آخر دون إذن. لا يجوز لك نشر أوصاف أو روابط أو طرق لسرقة الملكية الفكرية لشخص ما (البرامج أو الفيديوهات أو الملفات الصوتية أو الصور) أو لخرق أي قانون آخر.\n\n\n\n## [بدعمٍ منك](#power)\n\nيتم تشغيل هذا الموقع بواسطة [فريق العمل المحلي](%{base_path}/about) والمستخدمين *أمثالك*؛ أي المجتمع. إذا كانت لديك أي أسئلة أخرى بشأن كيفية عمل الأشياء هنا، فافتح موضوعًا جديدًا في [فئة ملاحظات الموقع](%{base_path}/c/site-feedback) ولنناقشها! إذا كانت هناك مشكلة حرجة أو عاجلة لا يمكن معالجتها من خلال موضوع أو علامة وصفية، فتواصل معنا من خلال [صفحة فريق العمل](%{base_path}/about).\n\n\n\n## [شروط الخدمة](#tos)\n\nنعم، الحديث القانوني ممل، ولكن يجب علينا حماية أنفسنا – وبالتبعية حمايتك أنت وبياناتك – ضد الأشخاص غير الودودين. لدينا [شروط الخدمة](%{base_path}/tos) التي تصف سلوكك (وسلوكنا) والحقوق المتعلقة بالمحتوى والخصوصية والقوانين. لاستخدام هذه الخدمة، يجب أن توافق على الالتزام بشروط الخدمة [TOS](%{base_path}/tos).\n"
tos_topic:
title: "شروط الخدمة"
body: |
@@ -4024,17 +4255,17 @@ ar:
يتم منحك هذه الشارة عندما تصل إلى مستوى الثقة 1. نشكرك على متابعة بعض الموضوعات وقراءتها لمعرفة ما يدور حوله مجتمعنا. تم رفع قيود المستخدم الجديد؛ لقد تم منحك جميع الوظائف الأساسية في المجتمع، مثل الرسائل الخاصة، والبلاغات، وتعديل Wiki، والقدرة على نشر صور وروابط متعددة.
member:
name: عضو
- description: تم منحك الدعوات والمراسلات الجماعية والمزيد من الإعجابات
+ description: تم منحك الدعوات والمراسلات الجماعية والمزيد من تسجيلات الإعجاب
long_description: |
- يتم منح هذه الشارة عندما تصل إلى مستوى الثقة 2. نشكرك على المشاركة على مدار أسابيع للانضمام حقًا إلى مجتمعنا. يمكنك الآن إرسال دعوات من صفحة المستخدم الخاصة بك أو من الموضوعات الفردية، وإنشاء رسائل خاصة جماعية، والحصول على المزيد من الإعجابات كل يوم.
+ يتم منح هذه الشارة عندما تصل إلى مستوى الثقة 2. نشكرك على المشاركة على مدار أسابيع للانضمام حقًا إلى مجتمعنا. يمكنك الآن إرسال دعوات من صفحة المستخدم الخاصة بك أو من الموضوعات الفردية، وإنشاء رسائل خاصة جماعية، والحصول على المزيد من تسجيلات الإعجاب كل يوم.
regular:
name: منتظم
- description: تم منحك إعادة التصنيف وإعادة التسمية والروابط التي تتم متابعتها، وWiki، والمزيد من الإعجابات
+ description: تم منحك إعادة التصنيف وإعادة التسمية والروابط التي تتم متابعتها، وWiki، والمزيد من تسجيلات الإعجاب
long_description: |
- يتم منح هذه الشارة عندما تصل إلى مستوى الثقة 3. نشكرك على كونك جزءًا منتظمًا من مجتمعنا على مدار أشهر. أنت الآن أحد القراء الأكثر نشاطًا ومساهمًا موثوقًا يجعل مجتمعنا رائعًا. يمكنك الآن إعادة تصنيف الموضوعات وإعادة تسميتها، والاستفادة ببلاغات أكثر قوة عن السلوك غير المرغوب فيه، والوصول إلى منطقة استراحة خاصة، وستحصل أيضًا على المزيد من الإعجابات كل يوم.
+ يتم منح هذه الشارة عندما تصل إلى مستوى الثقة 3. نشكرك على كونك جزءًا منتظمًا من مجتمعنا على مدار أشهر. أنت الآن أحد القراء الأكثر نشاطًا ومساهمًا موثوقًا يجعل مجتمعنا رائعًا. يمكنك الآن إعادة تصنيف الموضوعات وإعادة تسميتها، والاستفادة ببلاغات أكثر قوة عن السلوك غير المرغوب فيه، والوصول إلى منطقة استراحة خاصة، وستحصل أيضًا على المزيد من تسجيلات الإعجاب كل يوم.
leader:
name: قائد
- description: تم منحك التعديل الشامل والتثبيت والإغلاق والأرشفة والتقسيم والدمج والمزيد من الإعجابات
+ description: تم منحك التعديل الشامل والتثبيت والإغلاق والأرشفة والتقسيم والدمج والمزيد من تسجيلات الإعجاب
long_description: |
يتم منح هذه الشارة عندما تصل إلى مستوى الثقة 4. أنت قائد في هذا المجتمع كما حدَّد فريق العمل، وتقدِّم مثالًا إيجابيًا لبقية المجتمع في أفعالك وأقوالك هنا. لديك القدرة على تعديل جميع المنشورات، واتخاذ الإجراءات الشائعة التي يتخذها مشرف الموضوع، مثل التثبيت والإغلاق وإلغاء الإدراج والأرشفة والتقسيم والدمج.
welcome:
@@ -4231,7 +4462,7 @@ ar:
name: "المستخدم الجديد لهذا الشهر"
description: مساهمات متميزة في الشهر الأول
long_description: |
- يتم منح هذه الشارة لتهنئة مستخدمَين جديدين كل شهر على إجمالي مساهماتهما الممتازة، وفقًا لعدد الإعجابات على منشوراتهما ومن سجَّلوها.
+ يتم منح هذه الشارة لتهنئة مستخدمَين جديدين كل شهر على إجمالي مساهماتهما الممتازة، وفقًا لعدد تسجيلات الإعجاب على منشوراتهما ومن سجَّلوها.
enthusiast:
name: متحمس
description: الزيارة لمدة 10 أيام متتالية
@@ -4255,6 +4486,7 @@ ar:
invalid_token: "الرمز غير صالح."
email_input: "البريد الإلكتروني للمسؤول"
submit_button: "إرسال رسالة إلكترونية"
+ safe_mode: "الوضع الآمن: إيقاف كل السمات المكوِّنات الإضافية عند تسجيل الدخول"
performance_report:
initial_post_raw: يتضمَّن هذا الموضوع تقارير الأداء اليومية لموقعك.
initial_topic_title: تقارير أداء الموقع
@@ -4288,6 +4520,20 @@ ar:
other: '"%{tag_name}" مقصور على الفئات التالية: %{category_names}'
synonym: 'غير مسموح باستخدام المرادفات. استخدم "%{tag_name}" بدلًا منها.'
has_synonyms: 'لا يمكن استخدام "%{tag_name}" لأنه يحتوي على مرادفات.'
+ restricted_tags_cannot_be_used_in_category:
+ zero: 'لا يمكن استخدام الوسم "%{tags}" في فئة "%{category}". يُرجى إزالته.'
+ one: 'لا يمكن استخدام الوسم "%{tags}" في فئة "%{category}". يُرجى إزالته.'
+ two: 'لا يمكن استخدام الوسمين "%{tags}" في فئة "%{category}". يُرجى إزالتهما.'
+ few: 'لا يمكن استخدام الوسوم "%{tags}" في الفئة "%{category}". يُرجى إزالتها.'
+ many: 'لا يمكن استخدام الوسوم "%{tags}" في الفئة "%{category}". يُرجى إزالتها.'
+ other: 'لا يمكن استخدام الوسوم "%{tags}" في الفئة "%{category}". يُرجى إزالتها.'
+ category_does_not_allow_tags:
+ zero: 'لا تسمح الفئة "%{category}" بالوسم "%{tags}". يُرجى إزالته.'
+ one: 'لا تسمح الفئة "%{category}" بالوسم "%{tags}". يُرجى إزالته.'
+ two: 'لا تسمح الفئة "%{category}" بالوسمين "%{tags}". يُرجى إزالتهما.'
+ few: 'لا تسمح الفئة "%{category}" بالوسوم "%{tags}". يُرجى إزالتها.'
+ many: 'لا تسمح الفئة "%{category}" بالوسوم "%{tags}". يُرجى إزالتها.'
+ other: 'لا تسمح الفئة "%{category}" بالوسوم "%{tags}". يُرجى إزالتها.'
required_tags_from_group:
zero: "يجب عليك تضمين %{count}% وسم من %{tag_group_name} على الأقل. الوسوم في هذه المجموعة هي: %{tags}."
one: "يجب عليك تضمين وسم واحد (%{count}%) من %{tag_group_name} على الأقل. الوسوم في هذه المجموعة هي: %{tags}."
@@ -4303,6 +4549,7 @@ ar:
register:
button: "التسجيل"
title: "تسجيل حساب المسؤول"
+ help: "أنشئ حسابًا جديدًا للبدء."
no_emails: "للأسف، لم يتم تحديد أي رسائل إلكترونية للمسؤول في أثناء الإعداد؛ لذا قد يكون إنهاء الإعداد أمرًا صعبًا. يُرجى إضافة عنوان بريد إلكتروني للمطوِّر في ملف الإعداد أو إنشاء حساب مسؤول من وحدة التحكم ."
confirm_email:
title: "تأكيد عنوان بريدك الإلكتروني"
@@ -4312,6 +4559,8 @@ ar:
message: "
استمتع بوقتك!"
styling:
+ title: "الشكل والمظهر"
fields:
+ color_scheme:
+ label: "نظام الألوان"
body_font:
label: "خط النص الأساسي"
heading_font:
@@ -4345,6 +4610,7 @@ ar:
styling_preview:
label: "معاينة"
homepage_style:
+ label: "نمط الصفحة الرئيسية"
choices:
latest:
label: "أحدث الموضوعات"
@@ -4354,26 +4620,42 @@ ar:
label: "الفئات ذات الموضوعات المميزة"
categories_and_latest_topics:
label: "الفئات وأحدث الموضوعات"
+ categories_and_latest_topics_created_date:
+ label: "الفئات وأحدث الموضوعات (الفرز حسب تاريخ إنشاء الموضوع)"
categories_and_top_topics:
label: "الفئات والموضوعات الأكثر نشاطًا"
+ categories_boxes:
+ label: "مربعات الفئات"
+ categories_boxes_with_topics:
+ label: "مربعات الفئات ذات الموضوعات"
+ subcategories_with_featured_topics:
+ label: "الفئات الفرعية ذات الموضوعات المميزة"
branding:
+ title: "تخصيص الشعارات"
fields:
logo:
label: "الشعار الأساسي"
+ description: "الشعار في الجزء العلوي الأيمن من موقعك. استخدم صورة مستطيلة عريضة بارتفاع 120 ونسبة عرض إلى ارتفاع أكبر من 3:1"
logo_small:
label: "الشعار المربع"
+ description: "نسخة مربعة من شعارك. تظهر في الجزء العلوي الأيمن عند التمرير لأسفل، وعند المشاركة على المنصات الاجتماعية. يجب أن يكون الحجم المثالي 512×512 على الأقل."
favicon:
label: "أيقونة المتصفح"
+ description: "الأيقونة المستخدمة لتمثيل موقعك في متصفحات الويب وتبدو جيدة في الأحجام الصغيرة. يوصى بتنسيق PNG أو JPG. يُستخدَم الشعار المربع بشكلٍ افتراضي."
large_icon:
- label: "الأيقونة الكبيرة"
+ label: "أيقونة كبيرة"
+ description: "الأيقونة المستخدمة لتمثيل موقعك على أجهزة الجوَّال وتبدو جيدة في الأحجام الأكبر. يجب أن يكون الحجم المثالي أكبر من 512×512. سنستخدم الشعار المربع بشكلٍ افتراضي."
corporate:
+ title: "مؤسستك"
fields:
company_name:
label: "اسم الشركة"
placeholder: "مثال لمؤسسة"
+ description: "يتم إدخاله في صفحة شروط الخدمة. لا تتردَّد في التخطي إذا لم تكن هناك شركة."
governing_law:
label: "القانون المعمول به"
placeholder: "قانون ولاية كاليفورنيا"
+ description: "يتم إدخاله في صفحة شروط الخدمة. لا تتردَّد في التخطي إذا لم تكن هناك شركة."
contact_url:
label: "صفحة الويب"
placeholder: "https://www.example.com/contact-us"
@@ -4381,6 +4663,7 @@ ar:
city_for_disputes:
label: "مدينة النزاعات"
placeholder: "سان فرانسيسكو، كاليفورنيا"
+ description: "يتم إدخاله في صفحة شروط الخدمة. لا تتردَّد في التخطي إذا لم تكن هناك شركة."
site_contact:
label: "الرسائل التلقائية"
description: "سيتم إرسال جميع رسائل Discourse التلقائية الخاصة من هذا المستخدم، مثل التحذيرات بشأن البلاغات وإشعارات إكمال النسخ الاحتياطي."
@@ -4443,8 +4726,24 @@ ar:
missing_version: "يجب عليك توفير معلمة للإصدار"
conflict: "حدث تعارض في التحديث منعك من إجراء ذلك."
reasons:
+ post_count: "يجب الموافقة على أول بضعة منشورات من كل مستخدم من قِبل فريق العمل. راجع %{link}."
+ trust_level: "يجب أن تتم الموافقة على ردود المستخدمين من مستويات الثقة المنخفضة من قِبل فريق العمل. راجع %{link}."
+ new_topics_unless_trust_level: "يجب أن تتم الموافقة على موضوعات المستخدمين من مستويات الثقة المنخفضة من قِبل فريق العمل. راجع %{link}."
+ fast_typer: "كتب مستخدم جديد أول منشور له بسرعة كبيرة بشكلٍ مريب، ويُشتبَه في أن يكون روبوتًا أو صاحب سلوك غير مرغوب فيه. راجع %{link}."
+ auto_silence_regex: "مستخدم جديد يتطابق منشوره الأول مع الإعداد %{link}."
+ watched_word: "تضمَّن هذا المنشور كلمة مراقبة. راجع %{link}."
+ staged: "يجب الموافقة على الموضوعات والمنشورات الجديدة من المستخدمين المؤقتين من قِبل فريق العمل. راجع %{link}."
+ category: "تتطلب المنشورات في هذه الفئة موافقة يدوية بواسطة فريق العمل. راجع %{link}."
+ must_approve_users: "يجب الموافقة على جميع المستخدمين الجُدد من قِبل فريق العمل. راجع %{link}."
+ invite_only: "يجب دعوة جميع المستخدمين الجُدد. راجع %{link}."
email_auth_res_enqueue: "فشلت هذه الرسالة الإلكترونية في اجتياز تحقُّق DMARC، وعلى الأرجح أنه ليس من الشخص الذي يبدو أنه منه. تحقَّق من رؤوس الرسائل الإلكترونية البسيطة لمزيد من المعلومات."
+ email_spam: "تم الإبلاغ عن هذه الرسالة الإلكترونية كرسالة غير مرغوب فيها من خلال الرأس المحدَّد في %{link}."
+ suspect_user: "أدخل هذا المستخدم الجديد معلومات الملف الشخصي دون قراءة أي موضوعات أو منشورات، مما يشير بشدة إلى أنه قد يكون من أصحاب السلوك غير المرغوب فيه. راجع %{link}."
+ contains_media: "يحتوي هذا المنشور على وسائط مضمَّنة. راجع %{link}."
queued_by_staff: "يعتقد أحد أعضاء فريق العمل أن هذا المنشور يحتاج إلى مراجعة. سيظل مخفيًا حتى تتم مراجعته."
+ links:
+ watched_word: قائمة الكلمات المراقبة
+ category: إعدادات الفئة
actions:
agree:
title: "الموافقة..."
@@ -4528,6 +4827,8 @@ ar:
notification_level:
ignore_error: "عذرًا، لا يمكنك تجاهل هذا المستخدم."
mute_error: "عذرًا، لا يمكنك كتم هذا المستخدم."
+ error: "عذرًا، لا يمكنك تغيير مستوى الإشعارات لهذا المستخدم."
+ invalid_value: 'القيمة "%{value}" ليست مستوى إشعارات صالحًا.'
discord:
not_in_allowed_guild: "فشلت المصادقة. أنت لست عضوًا في خادم Discord مسموح به."
old_keys_reminder:
@@ -4550,7 +4851,7 @@ ar:
other: "%{topic_title} (الجزء %{count})"
post_raw: "متابعة المناقشة من %{parent_url}.\n\nالمناقشات السابقة:\n\n%{previous_topics}"
small_action_post_raw: "تابع المناقشة في %{new_title}."
- fallback_username: "مستخدم واحد"
+ fallback_username: "مستخدم"
user_status:
errors:
ends_at_should_be_greater_than_set_at: "يجب أن تكون end_at أكبر من set_at"
diff --git a/config/locales/server.ca.yml b/config/locales/server.ca.yml
index 9205e62714..57e331ee66 100644
--- a/config/locales/server.ca.yml
+++ b/config/locales/server.ca.yml
@@ -585,7 +585,6 @@ ca:
one: "fa gairebé %{count} any"
other: "fa gairebé %{count} any"
password_reset:
- no_token: "Aquest enllaç de canvi de contrasenya és massa antic. Trieu el botó d'inici de sessió i feu servir \"He oblidat la contrasenya\" per a obtenir un enllaç nou."
choose_new: "Trieu una contrasenya nova"
choose: "Trieu una contrasenya"
update: "Actualitza la contrasenya"
diff --git a/config/locales/server.da.yml b/config/locales/server.da.yml
index 91e920f6bb..64de6f1f4a 100644
--- a/config/locales/server.da.yml
+++ b/config/locales/server.da.yml
@@ -466,7 +466,6 @@ da:
Du kan redigere dit forrige svar for at tilføje et citat ved at markere teksten og vælge citér svar-knappen som dukker op.
Det er nemmere for alle at læse emner som har færre detaljerede svar i stedet for mange mindre, individuelle svar.
- dominating_topic: Du har skrevet mere end %{percent}% af svarene her, er der andre, som du gerne vil høre fra?
get_a_room: Du har svaret @%{reply_username} %{count} gange, vidste du, at du kunne sende dem en personlig besked i stedet?
too_many_replies: |
### Du kan ikke skrive flere indlæg i dette emne
@@ -699,7 +698,6 @@ da:
one: "næsten %{count} år siden"
other: "næsten %{count} år siden"
password_reset:
- no_token: "Beklager, dit skift-kodeords link er udløbet. Vælg 'Log ind' knappen og brug 'Jeg har glemt min adgangskode' for at få et nyt link."
choose_new: "Vælg en ny adgangskode"
choose: "Vælg en adgangskode"
update: "Opdatér Adgangskode"
diff --git a/config/locales/server.de.yml b/config/locales/server.de.yml
index 0192e02b8f..551584b6d5 100644
--- a/config/locales/server.de.yml
+++ b/config/locales/server.de.yml
@@ -495,7 +495,6 @@ de:
Du kannst deine letzte Antwort bearbeiten, um ein Zitat hinzuzufügen, indem du den Text auswählst und auf die erscheinende Schaltfläche Zitat klickst.
Es ist für alle einfacher, Themen zu lesen, die wenige umfassende Antworten statt viele kleine und einzelne Antworten haben.
- dominating_topic: Du hast mehr als %{percent} % der Antworten hier gepostet. Gib anderen doch auch die Möglichkeit, etwas beizutragen.
get_a_room: Du hast @%{reply_username} %{count} Mal geantwortet. Wusstest du, dass du der Person stattdessen eine persönliche Nachricht schicken kannst?
too_many_replies: |
### Du hast das Antwort-Limit für dieses Thema erreicht
@@ -764,7 +763,6 @@ de:
one: "vor fast %{count} Jahr"
other: "vor fast %{count} Jahren"
password_reset:
- no_token: "Entschuldige, aber der Link zum Ändern des Passworts ist zu alt. Wähle die Anmelden-Schaltfläche und nutze „Ich habe mein Passwort vergessen“, um einen neuen Link zu erhalten."
choose_new: "Wähle ein neues Passwort"
choose: "Wähle ein Passwort"
update: "Passwort aktualisieren"
@@ -3698,7 +3696,6 @@ de:
png_to_jpg_conversion_failure_message: "Beim Konvertieren von PNG in JPG ist ein Fehler aufgetreten."
optimize_failure_message: "Beim Optimieren des hochgeladenen Bildes ist ein Fehler aufgetreten."
download_failure: "Herunterladen der Datei vom externen Anbieter fehlgeschlagen."
- size_mismatch_failure: "Die Größe der auf S3 hochgeladenen Datei stimmte nicht mit der beabsichtigten Größe des externen Upload-Stubs überein. %{additional_detail}"
create_multipart_failure: "Fehler beim Erstellen eines mehrteiligen Uploads im externen Speicher."
abort_multipart_failure: "Fehler beim Abbrechen des mehrteiligen Uploads im externen Speicher."
complete_multipart_failure: "Der mehrteilige Upload im externen Speicher konnte nicht abgeschlossen werden."
diff --git a/config/locales/server.el.yml b/config/locales/server.el.yml
index 30fbff39f3..0ba48c1b79 100644
--- a/config/locales/server.el.yml
+++ b/config/locales/server.el.yml
@@ -543,7 +543,6 @@ el:
one: "σχεδόν %{count} χρόνο πριν"
other: "σχεδόν %{count} χρόνια πριν"
password_reset:
- no_token: "Συγνώμη, ο σύνδεσμος αλλαγής κωδικού είναι πολύ παλιός. Πατήστε ξανά το κουμπί 'Συνδεθείτε' και επιλέξτε 'Ξέχασα τον κωδικό πρόσβασής μου' για να λάβετε νέο σύνδεσμο. "
choose_new: "Επιλέξτε νέο κωδικό πρόσβασης"
choose: "Επιλέξτε έναν κωδικό πρόσβασης"
update: "Ενημέρωση κωδικού πρόσβασης"
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index e853452a01..67254e6218 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -697,6 +697,7 @@ en:
disallowed_topic_tags: "This topic has tags not allowed by this category: '%{tags}'"
disallowed_tags_generic: "This topic has disallowed tags."
slug_contains_non_ascii_chars: "contains non-ascii characters"
+ is_already_in_use: "is already in use"
cannot_delete:
uncategorized: "This category is special. It is intended as a holding area for topics that have no category; it cannot be deleted."
has_subcategories: "Can't delete this category because it has sub-categories."
@@ -1437,7 +1438,7 @@ en:
group_email_credentials_warning: 'There was an issue with the email credentials for the group %{group_full_name}. No emails will send from the group inbox until this problem is addressed. %{error}'
rails_env_warning: "Your server is running in %{env} mode."
host_names_warning: "Your config/database.yml file is using the default localhost hostname. Update it to use your site's hostname."
- sidekiq_warning: 'Sidekiq is not running. Many tasks, like sending emails, are executed asynchronously by sidekiq. Please ensure at least one sidekiq process is running. Learn about Sidekiq here.'
+ sidekiq_warning: 'Sidekiq is not running. Many tasks, like sending emails, are executed asynchronously by Sidekiq. Please ensure at least one Sidekiq process is running. Learn about Sidekiq here.'
queue_size_warning: "The number of queued jobs is %{queue_size}, which is high. This could indicate a problem with the Sidekiq process(es), or you may need to add more Sidekiq workers."
memory_warning: "Your server is running with less than 1 GB of total memory. At least 1 GB of memory is recommended."
google_oauth2_config_warning: 'The server is configured to allow signup and login with Google OAuth2 (enable_google_oauth2_logins), but the client id and client secret values are not set. Go to the Site Settings and update the settings. See this guide to learn more.'
@@ -2227,7 +2228,7 @@ en:
bootstrap_mode_min_users: "Minimum number of users required to disable bootstrap mode (set to 0 to disable)"
prevent_anons_from_downloading_files: "Prevent anonymous users from downloading attachments."
- secure_media: 'DEPRECATED: Use the secure_uploads setting instead, will be removed in Discourse 3.0.'
+ secure_media: "DEPRECATED: Use the secure_uploads setting instead, will be removed in Discourse 3.0."
secure_uploads: 'Limits access to ALL uploads (images, video, audio, text, pdfs, zips, and others). If "login required” is enabled, only logged-in users can access uploads. Otherwise, access will be limited only for media uploads in private messages and private categories. WARNING: This setting is complex and requires deep administrative understanding. See the secure uploads topic on Meta for details.'
secure_media_allow_embed_images_in_emails: "DEPRECATED: Use secure_uploads_allow_embed_images_in_emails, will remove in Discourse 3.0."
secure_uploads_allow_embed_images_in_emails: "Allows embedding secure images that would normally be redacted in emails, if their size is smaller than the 'secure uploads max email embed image size kb' setting."
@@ -2279,6 +2280,8 @@ en:
default_email_in_reply_to: "Include excerpt of replied to post in emails by default."
+ default_hide_profile_and_presence: "Hide user public profile and presence features by default."
+
default_other_new_topic_duration_minutes: "Global default condition for which a topic is considered new."
default_other_auto_track_topics_after_msecs: "Global default time before a topic is automatically tracked."
default_other_notification_level_when_replying: "Global default notification level when the user replies to a topic."
@@ -2351,7 +2354,7 @@ en:
sitemap_page_size: "Number of URLs to include in each sitemap page. Max 50.000"
enable_user_status: "(experimental) Allow users to set custom status message (emoji + description)."
- enable_onboarding_popups: "(experimental) Enable educational popups that describe key features to users"
+ enable_user_tips: "(experimental) Enable new user tips that describe key features to users"
short_title: "The short title will be used on the user's home screen, launcher, or other places where space may be limited. It should be limited to 12 characters."
diff --git a/config/locales/server.es.yml b/config/locales/server.es.yml
index 9bb1e6e0f3..d00775e7e0 100644
--- a/config/locales/server.es.yml
+++ b/config/locales/server.es.yml
@@ -495,7 +495,6 @@ es:
Puedes editar tu anterior respuesta para añadir una cita. Para ello, selecciona el texto que quieras citar y pulsa el botón citar respuesta que aparecerá.
Es más fácil leer temas que tengan menos respuestas (aunque más profundas), que tener que leer muchas respuestas individuales.
- dominating_topic: Has publicado más del %{percent} % de las respuestas, ¿hay alguien más de quien te gustaría saber?
get_a_room: Has respondido a @%{reply_username} %{count} veces, ¿sabías que también puedes enviarle un mensaje personal directamente?
too_many_replies: |
### Has llegado al límite de respuestas en este tema
@@ -764,7 +763,6 @@ es:
one: "hace casi %{count} año"
other: "hace casi %{count} años"
password_reset:
- no_token: "Lo sentimos, ese enlace para cambiar la contraseña es demasiado antiguo. Haz clic en el botón Iniciar sesión y utiliza el «olvidé mi contraseña» para obtener un nuevo enlace."
choose_new: "Escoge una nueva contraseña"
choose: "Escoge una contraseña"
update: "Actualizar contraseña"
@@ -3702,7 +3700,6 @@ es:
png_to_jpg_conversion_failure_message: "Ha ocurrido un error cuando se convertía desde PNG a JPG."
optimize_failure_message: "Ha ocurrido un error al optimizar la imagen subida."
download_failure: "Ha fallado la descarga del archivo del proveedor externo."
- size_mismatch_failure: "El tamaño del archivo subido a S3 no coincide con el tamaño esperado del stub externo. %{additional_detail}"
create_multipart_failure: "Fallo al crear una subida en varias partes en el almacenamiento externo."
abort_multipart_failure: "Fallo al abortar una subida en varias partes en el almacenamiento externo."
complete_multipart_failure: "Fallo al completar la subida en varias partes en el almacenamiento externo."
diff --git a/config/locales/server.et.yml b/config/locales/server.et.yml
index 5dd4ce33a9..65ad5c688c 100644
--- a/config/locales/server.et.yml
+++ b/config/locales/server.et.yml
@@ -399,7 +399,6 @@ et:
one: "peaaegu %{count} aasta tagasi"
other: "peaaegu %{count} aastat tagasi"
password_reset:
- no_token: "Vabandust, see parooli uuendamise link on liiga vana. Vajuta sisselogimise nuppu ja kasuta värske lingi saamiseks 'Unustasin parooli'."
choose_new: "Vali uus parool"
choose: "Vali parool"
update: "Uuenda parooli"
diff --git a/config/locales/server.fa_IR.yml b/config/locales/server.fa_IR.yml
index d912b7b0fb..3a73ebd215 100644
--- a/config/locales/server.fa_IR.yml
+++ b/config/locales/server.fa_IR.yml
@@ -498,7 +498,6 @@ fa_IR:
one: "تقریبا %{count} سال قبل"
other: "تقریبا %{count} سال قبل"
password_reset:
- no_token: "متآسفیم, پیوند تغییر رمز عبور بسیار قدیمی است. دکمه ورود را انتخاب کنید و از 'من رمز عبور خود را فراموش کرده ام' برای دریافت یک پیوند جدید استفاده کنید."
choose_new: "رمزعبور جدید را وارد کنید"
choose: "رمزعبور وارد کنید"
update: "بهروز کردن رمزعبور"
diff --git a/config/locales/server.fi.yml b/config/locales/server.fi.yml
index 860882692b..bb441a2e43 100644
--- a/config/locales/server.fi.yml
+++ b/config/locales/server.fi.yml
@@ -128,7 +128,7 @@ fi:
unsubscribe_not_allowed: "Näin käy, kun tämä käyttäjä ei voi perua tilausta sähköpostitse."
email_not_allowed: "Näin käy, kun sähköpostiosoite ei ole sallittujen listalla tai on kiellettyjen listalla."
unrecognized_error: "Tuntematon virhe"
- secure_uploads_placeholder: "Piilotettu: Tällä sivustolla on suojatut lataukset käytössä, vieraile ketjussa tai katso liitetyt lataukset napsauttamalla Näytä sisältö -painiketta."
+ secure_uploads_placeholder: "Piilotettu: Tällä sivustolla on suojatut lataukset käytössä, vieraile ketjussa tai katso liitetyt lataukset klikkaamalla Näytä sisältö -painiketta."
view_redacted_media: "Näytä sisältö"
errors: &errors
format: ! "%{attribute} %{message}"
@@ -495,7 +495,6 @@ fi:
Voit muokata edellistä viestiäsi ja lisätä siihen lainauksen maalaamalla lainattavan viestin tekstiä ja klikkaamalla ilmestyvää lainaa-painiketta.
On helpompaa lukea ketjua, jossa on vähemmän pidempiä vastauksia kuin sellaista, jossa on paljon lyhyitä yksittäisiä vastauksia.
- dominating_topic: Olet lähettänyt yli %{percent} % vastauksista täällä, olisiko aika antaa muiden osallistua keskusteluun?
get_a_room: Olet vastannut käyttäjälle @%{reply_username} %{count} kertaa, tiesitkö, että voit lähettää hänelle yksityisviestin?
too_many_replies: |
### Olet kirjoittanut enimmäismäärän vastauksia tähän ketjuun
@@ -654,10 +653,10 @@ fi:
post:
image_placeholder:
broken: "Tämä kuva ei toimi"
- blocked_hotlinked_title: "Kuva on isännöity toisella sivustolla. Avaa se uudessa välilehdessä napsauttamalla."
+ blocked_hotlinked_title: "Kuva on isännöity toisella sivustolla. Avaa se uudessa välilehdessä klikkaamalla."
blocked_hotlinked: "Ulkoinen kuva"
media_placeholder:
- blocked_hotlinked_title: "Mediasisältö on isännöity toisella sivustolla. Avaa se uudessa välilehdessä napsauttamalla."
+ blocked_hotlinked_title: "Mediasisältö on isännöity toisella sivustolla. Avaa se uudessa välilehdessä klikkaamalla."
blocked_hotlinked: "Ulkoinen mediasisältö"
hidden_bidi_character: "Kaksisuuntaiset merkit voivat muuttaa tekstin esitysjärjestystä. Tätä voidaan käyttää haitallisen koodin peittämiseen."
has_likes:
@@ -764,7 +763,6 @@ fi:
one: "lähes vuosi sitten"
other: "lähes %{count} vuotta sitten"
password_reset:
- no_token: "Tämä salasanan vaihtolinkki on liian vanha. Paina 'Kirjaudu sisään' -painiketta ja valitse 'Unohdin salasanani' saadaksesi uuden linkin."
choose_new: "Valitse uusi salasana"
choose: "Valitse salasana"
update: "Päivitä salasana"
@@ -3545,7 +3543,7 @@ fi:
title: "Vahvista uusi sähköpostiosoite"
subject_template: "[%{email_prefix}] Vahvista uusi sähköpostiosoite"
text_body_template: |
- Vahvista uusi sähköpostiosoitteesi sivustolla %{site_name} napsauttamalla seuraavaa linkkiä:
+ Vahvista uusi sähköpostiosoitteesi sivustolla %{site_name} klikkaamalla seuraavaa linkkiä:
%{base_url}/u/confirm-new-email/%{email_token}
@@ -3698,7 +3696,6 @@ fi:
png_to_jpg_conversion_failure_message: "PNG:n muuttamisessa JPG:ksi tapahtui virhe."
optimize_failure_message: "Ladatun kuvan optimoinnissa tapahtui virhe."
download_failure: "Tiedoston lataaminen ulkoiselta palveluntarjoajalta epäonnistui."
- size_mismatch_failure: "S3:een ladatun tiedoston koko ei vastannut ulkoisen latauksen tyngän tarkoitettua kokoa. %{additional_detail}"
create_multipart_failure: "Moniosaisen latauksen luominen ulkoisessa tallennustilassa epäonnistui."
abort_multipart_failure: "Moniosaisen latauksen hylkääminen ulkoisessa tallennustilassa epäonnistui."
complete_multipart_failure: "Moniosaisen latauksen viimeistely ulkoisessa tallennustilassa epäonnistui."
diff --git a/config/locales/server.fr.yml b/config/locales/server.fr.yml
index 8b6358ca10..5f8b8e2e43 100644
--- a/config/locales/server.fr.yml
+++ b/config/locales/server.fr.yml
@@ -101,7 +101,7 @@ fr:
incoming:
default_subject: "Ce sujet a besoin d'un titre"
show_trimmed_content: "Montrer le contenu raccourci"
- maximum_staged_user_per_email_reached: "Vous avez atteint le nombre maximal d'utilisateurs distants qui peuvent être créés par courriel."
+ maximum_staged_user_per_email_reached: "Vous avez atteint le nombre maximal d'utilisateurs distants qui peuvent être créés par e-mail."
no_subject: "(aucun objet)"
no_body: "(aucun texte)"
missing_attachment: "(La pièce jointe %{filename} est manquante)"
@@ -109,24 +109,24 @@ fr:
one: "Poursuite de la discussion à partir de [%{title}](%{url}), car elle a été créée il y a plus de %{count} jour."
other: "Poursuite de la discussion à partir de [%{title}](%{url}), car elle a été créée il y a plus de %{count} jours."
errors:
- empty_email_error: "Se produit quand le courriel reçu est vide."
- no_message_id_error: "Se produit quand le courriel n'a pas d'en-tête « Message-Id »."
+ empty_email_error: "Se produit quand l'e-mail reçu est vide."
+ no_message_id_error: "Se produit quand l'e-mail n'a pas d'en-tête « Message-Id »."
auto_generated_email_error: "Se produit quand l'en-tête « precedence » est : list, junk, bulk ou auto-reply, ou lorsque n'importe quel autre en-tête contient : auto-submitted, auto-replied ou auto-generated."
no_body_detected_error: "Se produit quand il est impossible d'extraire le corps du message et qu'il n'y a pas de pièces jointes."
- no_sender_detected_error: "Se produit lorsque nous n'avons pas trouvé une adresse courriel valide dans l'en-tête From."
- from_reply_by_address_error: "Survient quand l'en-tête « From » correspond à l'adresse courriel « Reply by »."
+ no_sender_detected_error: "Se produit lorsque nous n'avons pas trouvé une adresse e-mail valide dans l'en-tête From."
+ from_reply_by_address_error: "Survient quand l'en-tête « From » correspond à l'adresse e-mail « Reply by »."
inactive_user_error: "Se produit quand l'expéditeur n'est pas actif."
silenced_user_error: "Se produit lorsque l'expéditeur a été mis en sourdine."
- bad_destination_address: "Se produit quand aucune des adresses courriel reprises dans les champs To/Cc ne correspond à une adresse courriel entrante configurée."
+ bad_destination_address: "Se produit quand aucune des adresses e-mail reprises dans les champs To/Cc ne correspond à une adresse e-mail entrante configurée."
strangers_not_allowed_error: "Se produit quand un utilisateur essaie de créer un nouveau sujet dans une catégorie dans laquelle il n'est pas membre."
insufficient_trust_level_error: "Se produit quand un utilisateur essaie de créer un nouveau sujet dans une catégorie pour laquelle il n'a pas le niveau de confiance nécessaire."
- reply_user_not_matching_error: "Se produit quand une réponse est venue d'une adresse courriel différente de celle où a été envoyée la notification."
+ reply_user_not_matching_error: "Se produit quand une réponse est venue d'une adresse e-mail différente de celle où a été envoyée la notification."
topic_not_found_error: "Se produit quand quelqu'un répond à un sujet qui a été supprimé."
topic_closed_error: "Se produit quand quelqu'un répond alors que le sujet lié a été fermé."
- bounced_email_error: "Le courriel est un rapport de courriel non délivré."
- screened_email_error: "Se produit quand l'adresse courriel de l'expéditeur est déjà sous surveillance."
- unsubscribe_not_allowed: "Se produit lorsque le désabonnement via courriel n'est pas autorisé pour cet utilisateur."
- email_not_allowed: "Se produit quand l'adresse courriel n'est pas reprise dans la liste autorisée ou se trouve dans la liste des expéditeurs indésirables."
+ bounced_email_error: "L'e-mail est un rapport d'e-mail non délivré."
+ screened_email_error: "Se produit quand l'adresse e-mail de l'expéditeur est déjà sous surveillance."
+ unsubscribe_not_allowed: "Se produit lorsque le désabonnement par e-mail n'est pas autorisé pour cet utilisateur."
+ email_not_allowed: "Se produit quand l'adresse e-mail n'est pas reprise dans la liste autorisée ou se trouve dans la liste des expéditeurs indésirables."
unrecognized_error: "Erreur non reconnue"
secure_uploads_placeholder: "Masqué : ce site a activé la sécurisation des téléversements. Visitez le sujet ou cliquez sur « Afficher le contenu multimédia » pour les voir."
view_redacted_media: "Afficher le média"
@@ -223,7 +223,7 @@ fr:
expired: "Votre jeton d'invitation a expiré. Veuillez contacter les responsables."
not_found: "Votre jeton d'invitation est invalide. Veuillez contacter les responsables."
not_found_json: "Votre jeton d'invitation est invalide. Veuillez contacter les responsables."
- not_matching_email: "Votre adresse de courriel ne correspond pas à celle qui a été définie dans votre invitation. Nous vous invitons à contacter un responsable."
+ not_matching_email: "Votre adresse e-mail ne correspond pas à celle qui a été définie dans votre invitation. Nous vous invitons à contacter un responsable."
not_found_template: |
お楽しみください!"
+ description: "以上です!コミュニティーを設定するための基本を完了しました。後は、コミュニティーにアクセスして見て回り、ウェルカムトピックを書いて招待状を送信しましょう!
お楽しみください!"
styling:
title: "外観"
fields:
@@ -4460,14 +4457,14 @@ ja:
description: "通報の警告やバックアップ完了の通知など、Discourse のすべての自動個人メッセージはこのユーザーから送信されます。"
invites:
title: "スタッフの招待"
- description: "ほぼ完了です!コミュニティを始動させるために、興味深いトピックや返信でディスカッションを生み出せる人を何人が招待しましょう。"
+ description: "ほぼ完了です!コミュニティーを始動させるために、興味深いトピックや返信でディスカッションを生み出せる人を何人が招待しましょう。"
disabled: "ローカルログインが無効になっているため、誰にも招待を送れません。次のステップに進んでください。"
finished:
title: "あなたの Discourse の準備が整いました!"
description: |
@@ -534,7 +534,7 @@ nl:
admin_quick_start_title: "LEES MIJ EERST: Snelstartgids voor beheerders"
category:
topic_prefix: "Over de categorie %{category}"
- replace_paragraph: "(Vervang deze eerste alinea door een korte beschrijving van uw nieuwe categorie. Deze leidraad verschijnt in het categorieselectiegebied, dus probeer deze onder de 200 tekens te houden.)"
+ replace_paragraph: "(Vervang deze eerste alinea door een korte beschrijving van de nieuwe categorie. Deze leidraad wordt weergegeven in het categorieselectiegebied, dus probeer deze onder de 200 tekens te houden.)"
post_template: "%{replace_paragraph}\n\nGebruik de volgende alinea's voor een langere beschrijving, of om richtlijnen of regels voor de categorie op te stellen:\n\n- Waarom zouden mensen deze categorie gebruiken? Waar dient deze voor?\n\n- Op welke punten onderscheidt deze categorie zich van andere categorieën die we al hebben?\n\n- Wat dienen topics in deze categorie over het algemeen te bevatten?\n\n- Hebben we deze categorie nodig? Kunnen we deze samenvoegen met een andere categorie of subcategorie?\n"
errors:
not_found: "Categorie niet gevonden!"
@@ -652,16 +652,15 @@ nl:
one: "bijna %{count} jaar geleden"
other: "bijna %{count} jaar geleden"
password_reset:
- no_token: "Sorry, die koppeling voor het wijzigen van uw wachtwoord is te oud. Selecteer de knop Aanmelden en gebruik 'Ik ben mijn wachtwoord vergeten' om een nieuwe koppeling te ontvangen."
choose_new: "Een nieuw wachtwoord kiezen"
choose: "Een wachtwoord kiezen"
update: "Wachtwoord bijwerken"
save: "Wachtwoord instellen"
- title: "Wachtwoord herinitialiseren"
- success: "U hebt uw wachtwoord succesvol gewijzigd en bent nu aangemeld."
- success_unapproved: "U hebt uw wachtwoord succesvol gewijzigd."
+ title: "Wachtwoord herstellen"
+ success: "U hebt uw wachtwoord gewijzigd en bent nu aangemeld."
+ success_unapproved: "U hebt uw wachtwoord gewijzigd."
email_login:
- invalid_token: "Sorry, die koppeling voor het aanmelden via e-mail is te oud. Selecteer de knop Aanmelden en gebruik 'Ik ben mijn wachtwoord vergeten' om een nieuwe koppeling te ontvangen."
+ invalid_token: "Sorry, die e-mailaanmeldlink is te oud. Selecteer de knop Aanmelden en gebruik 'Ik ben mijn wachtwoord vergeten' om een nieuwe link te ontvangen."
title: "E-mailaanmelding"
user_auth_tokens:
browser:
@@ -699,7 +698,7 @@ nl:
error: "Er is een fout opgetreden bij het wijzigen van uw e-mailadres. Misschien is het adres al in gebruik?"
doesnt_exist: "Dat e-mailadres is niet aan uw account gekoppeld."
error_staged: "Er is een fout opgetreden bij het wijzigen van uw e-mailadres. Het adres is al in gebruik door een staged gebruiker."
- already_done: "Sorry, deze bevestigingskoppeling is niet meer geldig. Misschien is uw e-mailadres al gewijzigd?"
+ already_done: "Sorry, deze bevestigingslink is niet meer geldig. Misschien is uw e-mailadres al gewijzigd?"
confirm: "Bevestigen"
authorizing_new:
title: "Bevestig uw nieuwe e-mailadres"
@@ -709,13 +708,13 @@ nl:
old_email: "Oude e-mailadres: %{email}"
new_email: "Nieuwe e-mailadres: %{email}"
almost_done_title: "Nieuwe e-mailadres bevestigen"
- almost_done_description: "Er is een e-mail naar uw nieuwe e-mailadres verstuurd om de wijziging te bevestigen!"
+ almost_done_description: "Er is een e-mail naar uw nieuwe e-mailadres gestuurd om de wijziging te bevestigen!"
associated_accounts:
revoke_failed: "Intrekken van uw account bij %{provider_name} is mislukt."
connected: "(verbonden)"
activation:
action: "Klik hier om uw account te activeren"
- already_done: "Sorry, deze koppeling voor het bevestigen van uw account is niet meer geldig. Misschien is uw account al actief?"
+ already_done: "Sorry, deze link voor het bevestigen van uw account is niet meer geldig. Misschien is uw account al actief?"
please_continue: "Uw account is bevestigd; u wordt nu doorgestuurd naar de startpagina."
continue_button: "Doorgaan naar %{site_name}"
welcome_to: "Welkom bij %{site_name}!"
@@ -738,14 +737,14 @@ nl:
short_description: "Niet relevant voor de discussie"
spam:
title: "Spam"
- description: "Dit bericht is een advertentie of vandalisme. Het is niet relevant voor het huidige onderwerp."
+ description: "Dit bericht is een advertentie of vandalisme. Het is niet relevant voor het huidige topic."
short_description: "Dit is een advertentie of vandalisme"
email_title: '''%{title}'' is als spam gemarkeerd'
email_body: "%{link}\n\n%{message}"
inappropriate:
title: "Ongepast"
- description: 'Dit bericht bevat inhoud die een redelijk persoon als beledigend, kwetsend of een overtreding van onze gemeenschapsrichtlijnen zou beschouwen.'
- short_description: 'Een overtreding van onze gemeenschapsrichtlijnen'
+ description: 'Dit bericht bevat inhoud die een redelijk persoon als beledigend, kwetsend of een overtreding van onze communityrichtlijnen zou beschouwen.'
+ short_description: 'Een overtreding van onze communityrichtlijnen'
notify_user:
title: "@%{username} een bericht sturen"
description: "I wil rechtstreeks en persoonlijk met deze persoon over zijn of haar bericht praten."
@@ -780,7 +779,7 @@ nl:
invalid_origin_error: "De oorsprong van de authenticatieaanvraag komt niet overeen met de serveroorsprong."
malformed_attestation_error: "Er is een fout opgetreden bij het decoderen van de attestgegevens."
invalid_relying_party_id_error: "De Relying Party-ID van de authenticatieaanvraag komt niet overeen met de Relying Party-ID van de server."
- user_verification_error: "Er is gebruikersverificatie vereist."
+ user_verification_error: "Gebruikersverificatie is vereist."
unsupported_public_key_algorithm_error: "Het opgegeven algoritme van de openbare sleutel wordt niet ondersteund door de server."
unsupported_attestation_format_error: "De attestation-indeling wordt niet ondersteund door de server."
credential_id_in_use_error: "De opgegeven referentie-ID is al in gebruik."
@@ -796,9 +795,9 @@ nl:
short_description: "Dit is een advertentie"
inappropriate:
title: "Ongepast"
- description: 'Dit topic bevat inhoud die een redelijk persoon als beledigend, kwetsend of een overtreding van onze gemeenschapsrichtlijnen zou beschouwen.'
+ description: 'Dit topic bevat inhoud die een redelijk persoon als beledigend, kwetsend of een overtreding van onze communityrichtlijnen zou beschouwen.'
long_form: "heeft dit als ongepast gemarkeerd"
- short_description: 'Een overtreding van onze gemeenschapsrichtlijnen'
+ short_description: 'Een overtreding van onze communityrichtlijnen'
notify_moderators:
title: "Iets anders"
description: 'Dit topic vereist algemene aandacht van een staflid op basis van de richtlijnen, TOS, of om een andere reden dan hierboven vermeld.'
@@ -807,8 +806,8 @@ nl:
email_title: 'Het topic ''%{title}'' vereist aandacht van een moderator'
email_body: "%{link}\n\n%{message}"
flagging:
- you_must_edit: '
Opmerking: scripts van derden die door GTM worden geladen, dienen mogelijk op de acceptatielijst te worden geplaatst in 'content security policy script src'."
enable_escaped_fragments: "Terugvallen naar Google's Ajax-Crawling-API als geen webcrawler wordt gedetecteerd. Zie https://developers.google.com/webmasters/ajax-crawling/docs/learn-more"
cors_origins: "Toegestane domeinen voor cross-origin-aanvragen (CORS). Elk domein moet http:// of https:// bevatten. De omgevingsvariabele DISCOURSE_ENABLE_CORS moet op true zijn ingesteld om CORS in te schakelen."
- use_admin_ip_allowlist: "Beheerders kunnen zich alleen aanmelden vanaf een IP-adres dat in de lijst Gecontroleerde IP-nummers is gedefinieerd (Beheer > Logboeken > Gecontroleerde IP-adressen)."
+ use_admin_ip_allowlist: "Beheerders kunnen zich alleen aanmelden vanaf een IP-adres dat is gedefinieerd in de lijst Gecontroleerde IP-nummers (Beheer > Logs > Gecontroleerde IP-adressen)."
blocked_ip_blocks: "Een lijst van private IP-blokken die nooit door Discourse mogen worden verkend"
allowed_internal_hosts: "Een lijst van interne hosts die Discourse veilig kan verkennen voor oneboxing en andere doeleinden"
allowed_iframes: "Een lijst van 'iframe src'-domeinvoorvoegsels die Discourse veilig kan toestaan in berichten"
@@ -1360,10 +1359,10 @@ nl:
content_security_policy_collect_reports: "Rapportverzameling voor CSP-schendingen inschakelen via /csp_reports"
content_security_policy_script_src: "Aanvullende scriptbronnen op de acceptatielijst. De huidige host en CDN zijn standaard inbegrepen. Zie Mitigate XSS Attacks with Content Security Policy."
invalidate_inactive_admin_email_after_days: "Beheerdersaccounts die de website dit aantal dagen niet hebben bezocht, dienen hun e-mailadres voor het aanmelden opnieuw te valideren. Stel dit in op 0 om uit te schakelen."
- top_menu: "Bepalen welke items in het hoofdnavigatiemenu verschijnen, en in welke volgorde. Voorbeeld: latest|new|unread|categories|top|read|posted|bookmarks"
- post_menu: "Bepalen welke items in het berichtmenu verschijnen, en in welke volgorde. Voorbeeld: like|edit|flag|delete|share|bookmark|reply"
+ top_menu: "Bepaal welke items in het hoofdnavigatiemenu worden weergegeven en in welke volgorde. Voorbeeld: latest|new|unread|categories|top|read|posted|bookmarks"
+ post_menu: "Bepaal welke items in het berichtmenu worden weergegeven en in welke volgorde. Voorbeeld: like|edit|flag|delete|share|bookmark|reply"
post_menu_hidden_items: "De menu-items die standaard in het berichtmenu moeten worden verborgen, totdat er op een uitvouw-ellipsis wordt geklikt."
- share_links: "Bepalen welke items in het deelmenu verschijnen, en in welke volgorde."
+ share_links: "Bepaal welke items in de deeldialoog worden weergegeven en in welke volgorde."
site_contact_username: "Een geldige gebruikersnaam van een staflid waarvandaan alle automatische berichten worden verzonden. Wanneer leeg gelaten, wordt de standaardaccount van het systeem gebruikt."
site_contact_group_name: "Een geldige groepsnaam voor uitnodiging in alle automatische berichten."
send_welcome_message: "Alle nieuwe gebruikers een welkomstbericht met een snelstartgids sturen."
@@ -1403,14 +1402,14 @@ nl:
password_unique_characters: "Minimale aantal unieke tekens dat een wachtwoord moet hebben."
block_common_passwords: "Geen wachtwoorden toestaan die in de 10.000 meest gebruikte wachtwoorden voorkomen."
discourse_connect_overrides_bio: "Overschrijft biografie in gebruikersprofiel en voorkomt dat gebruiker deze kan wijzigen"
- enable_local_logins_via_email: "Gebruikers mogen vragen een koppeling voor aanmelding met één klik via e-mail te laten toesturen."
+ enable_local_logins_via_email: "Sta gebruikers toe te verzoeken een aanmeldlink met één klik te ontvangen via e-mail."
allow_new_registrations: "Nieuwe gebruikersregistraties toestaan. Vink dit uit om te voorkomen dat iedereen een nieuwe account kan maken."
enable_signup_cta: "Een melding tonen voor terugkerende anonieme gebruikers waarin wordt gevraagd zich voor een account te registreren"
enable_google_oauth2_logins: "Google Oauth2-authenticatie inschakelen. Dit is de methode voor authenticatie die Google momenteel ondersteunt. Vereist sleutel en geheim. Zie Configuring Google login for Discourse."
google_oauth2_client_id: "Client-ID van uw Google-toepassing."
google_oauth2_client_secret: "Client-geheim van uw Google-toepassing."
google_oauth2_prompt: "Een optionele door spaties gescheiden lijst van tekenreekswaarden die bepaalt of de autorisatieserver de gebruiker opnieuw om authenticatie en instemming vraagt. Zie https://developers.google.com/identity/protocols/OpenIDConnect#prompt voor de mogelijke waarden."
- google_oauth2_hd: "Een optioneel door Google Apps gehost domein waartoe de aanmelding wordt beperkt. Zie https://developers.google.com/identity/protocols/OpenIDConnect#hd-param voor meer details."
+ google_oauth2_hd: "Een optioneel door Google Apps gehost domein waartoe de aanmelding wordt beperkt. Zie https://developers.google.com/identity/protocols/OpenIDConnect#hd-param voor meer informatie."
enable_twitter_logins: "Twitter-authenticatie inschakelen, vereist twitter_consumer_key en twitter_consumer_secret. Zie Configuring Twitter login (and rich embeds) for Discourse."
twitter_consumer_key: "Consumentsleutel voor Twitter-authenticatie, geregistreerd bij https://developer.twitter.com/apps"
twitter_consumer_secret: "Consumentgeheim voor Twitter-authenticatie, geregistreerd bij https://developer.twitter.com/apps"
@@ -1439,9 +1438,9 @@ nl:
active_user_rate_limit_secs: "Hoe vaak we het 'last_seen_at'-veld bijwerken, in seconden."
verbose_localization: "Uitgebreide lokalisatietips in de gebruikersinterface tonen"
previous_visit_timeout_hours: "Hoe lang een bezoek duurt voordat we het als het 'vorige' bezoek beschouwen, in uren."
- top_topics_formula_log_views_multiplier: "waarde van vermenigvuldiger (n) voor logboekweergaven in toptopicsformule: `log(views_count) * (n) + op_likes_count * 0.5 + LEAST(likes_count / posts_count, 3) + 10 + log(posts_count)`"
- top_topics_formula_first_post_likes_multiplier: "waarde van vermenigvuldiger (n) voor likes van eerste berichten in toptopicsformule: `log(views_count) * 2 + op_likes_count * (n) + LEAST(likes_count / posts_count, 3) + 10 + log(posts_count)`"
- top_topics_formula_least_likes_per_post_multiplier: "waarde van vermenigvuldiger (n) voor minste likes per bericht in toptopicsformule: `log(views_count) * 2 + op_likes_count * 0.5 + LEAST(likes_count / posts_count, (n)) + 10 + log(posts_count)`"
+ top_topics_formula_log_views_multiplier: "waarde van vermenigvuldiger (n) voor logweergaven in toptopicsformule: 'log(views_count) * (n) + op_likes_count * 0.5 + LEAST(likes_count / posts_count, 3) + 10 + log(posts_count)'"
+ top_topics_formula_first_post_likes_multiplier: "waarde van vermenigvuldiger (n) voor likes van eerste berichten in toptopicsformule: 'log(views_count) * 2 + op_likes_count * (n) + LEAST(likes_count / posts_count, 3) + 10 + log(posts_count)'"
+ top_topics_formula_least_likes_per_post_multiplier: "waarde van vermenigvuldiger (n) voor minste likes per bericht in toptopicsformule: 'log(views_count) * 2 + op_likes_count * 0.5 + LEAST(likes_count / posts_count, (n)) + 10 + log(posts_count)'"
enable_safe_mode: "Gebruikers mogen de veilige modus betreden om plug-ins te debuggen."
rate_limit_create_topic: "Na het maken van een topic moeten gebruikers (n) seconden wachten voordat ze een ander topic kunnen maken."
rate_limit_create_post: "Na het plaatsen van een bericht moeten gebruikers (n) seconden wachten voor ze een ander bericht kunnen plaatsen."
@@ -1456,7 +1455,7 @@ nl:
max_topic_invitations_per_day: "Maximale aantal uitnodigingen voor een topic dat een gebruiker per dag kan versturen."
max_logins_per_ip_per_hour: "Maximale aantal toegestane aanmeldingen per IP-adres per uur"
max_logins_per_ip_per_minute: "Maximale aantal toegestane aanmeldingen per IP-adres per minuut"
- invite_link_max_redemptions_limit: "Het maximaal toegestane aantal inwisselingen voor uitnodigingskoppelingen kan niet meer zijn dan deze waarde."
+ invite_link_max_redemptions_limit: "Het maximaal toegestane aantal verzilveringen van uitnodigingslinks mag niet hoger zijn dan deze waarde."
alert_admins_if_errors_per_minute: "Aantal foutmeldingen per minuut waarbij de beheerder een melding krijgt. Een waarde van 0 schakelt deze functie uit. OPMERKING: vereist herstart."
alert_admins_if_errors_per_hour: "Aantal foutmeldingen per uur waarbij de beheerder een melding krijgt. Een waarde van 0 schakelt deze functie uit. OPMERKING: vereist herstart."
categories_topics: "Aantal topics dat in de /categories-pagina wordt getoond. Als dit op 0 is ingesteld, wordt automatisch naar een waarde gezocht die de twee kolommen symmetrisch houdt (categorieën en topics)."
@@ -1500,20 +1499,20 @@ nl:
tl2_requires_likes_received: "Hoeveel likes een gebruiker moet ontvangen voordat deze naar vertrouwensniveau 2 wordt gepromoveerd."
tl2_requires_likes_given: "Hoeveel likes een gebruiker moet geven voordat deze naar vertrouwensniveau 2 wordt gepromoveerd."
tl2_requires_topic_reply_count: "Op hoeveel topics een gebruiker moet antwoorden voordat deze naar vertrouwensniveau 2 wordt gepromoveerd."
- tl3_time_period: "Tijdsperiode voor vereisten voor vertrouwensniveau 3 (in dagen)"
+ tl3_time_period: "Tijd voor vereisten voor vertrouwensniveau 3 (in dagen)"
tl3_requires_days_visited: "Minimale aantal dagen dat een gebruiker de website in de afgelopen (tl3 time period) dagen moet hebben bezocht om voor promotie naar vertrouwensniveau 3 in aanmerking te komen. Stel dit hoger in dan tl3 time period om promoties naar tl3 uit te schakelen. (0 of hoger)"
tl3_requires_topics_replied_to: "Minimale aantal topics waarop een gebruiker in de afgelopen (tl3 time period) dagen moet hebben geantwoord om voor promotie naar vertrouwensniveau 3 in aanmerking te komen. (0 of hoger)"
tl3_requires_topics_viewed: "Het percentage in de afgelopen (tl3 time period) dagen gemaakte topics dat een gebruiker moet hebben bekeken om voor promotie naar vertrouwensniveau 3 in aanmerking te komen. (0 tot 100)"
- tl3_requires_topics_viewed_cap: "Het maximaal vereiste aantal topics dat in de afgelopen (tl3 time period) dagen is bekeken."
+ tl3_requires_topics_viewed_cap: "Het maximaal vereiste aantal topics dat de afgelopen (tl3 time period) dagen is bekeken."
tl3_requires_posts_read: "Het percentage in de afgelopen (tl3 time period) dagen gemaakte berichten dat een gebruiker moet hebben bekeken om voor promotie naar vertrouwensniveau 3 in aanmerking te komen. (0 tot 100)"
- tl3_requires_posts_read_cap: "Het maximaal vereiste aantal berichten dat in de afgelopen (tl3 time period) dagen is bekeken."
+ tl3_requires_posts_read_cap: "Het maximaal vereiste aantal berichten dat de afgelopen (tl3 time period) dagen is bekeken."
tl3_requires_topics_viewed_all_time: "Het minimale totale aantal topics dat een gebruiker moet hebben bekeken om voor vertrouwensniveau 3 in aanmerking te komen."
tl3_requires_posts_read_all_time: "Het minimale totale aantal berichten dat een gebruiker moet hebben gelezen om voor vertrouwensniveau 3 in aanmerking te komen."
tl3_requires_max_flagged: "Een gebruiker mag niet meer dan x berichten gemarkeerd door x verschillende gebruikers in de afgelopen (tl3 time period) dagen hebben gehad om voor promotie naar vertrouwensniveau 3 in aanmerking te komen, waarin x de waarde van de instelling is. (0 of hoger)"
tl3_promotion_min_duration: "Het minimale aantal dagen dat een promotie naar vertrouwensniveau 3 duurt voordat een gebruiker naar vertrouwensniveau 2 kan worden gedegradeerd."
tl3_requires_likes_given: "Het minimale aantal likes dat in de afgelopen (tl3 time period) dagen moet worden gegeven om voor promotie naar vertrouwensniveau 3 in aanmerking te komen."
tl3_requires_likes_received: "Het minimale aantal likes dat in de afgelopen (tl3 time period) dagen moet worden ontvangen om voor promotie naar vertrouwensniveau 3 in aanmerking te komen."
- tl3_links_no_follow: "rel=nofollow niet uit koppelingen verwijderen die door gebruikers met vertrouwensniveau 3 zijn geplaatst."
+ tl3_links_no_follow: "Verwijder rel=nofollow niet uit links die door gebruikers met vertrouwensniveau 3 zijn geplaatst."
trusted_users_can_edit_others: "Gebruikers met hoge vertrouwensniveaus mogen inhoud van andere gebruikers bewerken"
min_trust_to_create_topic: "Het minimale vertrouwensniveau dat nodig is om een topic te maken."
allow_flagging_staff: "Wanneer ingeschakeld, kunnen gebruikers berichten van stafaccounts markeren."
@@ -1521,19 +1520,19 @@ nl:
min_trust_to_edit_post: "Het minimale vertrouwensniveau dat nodig is om berichten te bewerken."
min_trust_to_allow_self_wiki: "Het minimale vertrouwensniveau dat nodig is om een gebruiker een eigen bericht naar een wiki te laten omzetten."
min_trust_to_flag_posts: "Het minimale vertrouwensniveau dat nodig is om berichten te markeren"
- min_trust_to_post_links: "Het minimale vertrouwensniveau dat nodig is om koppelingen in berichten op te nemen"
+ min_trust_to_post_links: "Het minimale vertrouwensniveau dat nodig is om links op te nemen in berichten"
min_trust_to_post_embedded_media: "Het minimale vertrouwensniveau dat nodig is om media-items in een bericht in te bedden"
min_trust_level_to_allow_profile_background: "Het minimale vertrouwensniveau dat nodig is om een profielachtergrond te uploaden"
min_trust_level_to_allow_user_card_background: "Het minimale vertrouwensniveau dat nodig is om een gebruikerskaartachtergrond te uploaden"
min_trust_level_to_allow_invite: "Het minimale vertrouwensniveau dat nodig is om gebruikers uit te nodigen"
min_trust_level_to_allow_ignore: "Het minimale vertrouwensniveau dat nodig is om gebruikers te negeren"
- allowed_link_domains: "Domeinen waarnaar gebruikers mogen verwijzen, zelfs als ze niet het juiste vertrouwensniveau hebben om koppelingen te plaatsen"
- newuser_max_links: "Hoeveel koppelingen een nieuwe gebruiker aan een bericht kan toevoegen."
+ allowed_link_domains: "Domeinen waar gebruikers naartoe mogen linken, ook als ze niet het juiste vertrouwensniveau hebben om links te plaatsen"
+ newuser_max_links: "Hoeveel links een nieuwe gebruiker kan toevoegen aan een bericht."
newuser_max_attachments: "Hoeveel bijlagen een nieuwe gebruiker aan een bericht kan toevoegen."
newuser_max_mentions_per_post: "Maximale aantal @naam-vermeldingen dat een nieuwe gebruiker in een bericht kan gebruiken."
newuser_max_replies_per_topic: "Maximale aantal antwoorden dat een nieuwe gebruiker in één topic kan plaatsen totdat iemand ze beantwoordt."
max_mentions_per_post: "Maximale aantal @naam-vermeldingen dat iedereen in een bericht kan gebruiken."
- max_users_notified_per_group_mention: "Maximale aantal gebruikers dat een melding kan ontvangen als een groep wordt genoemd (bij bereiken van drempel worden geen meldingen verstuurd)"
+ max_users_notified_per_group_mention: "Maximale aantal gebruikers dat een melding kan ontvangen als een groep wordt genoemd (bij bereiken van drempel worden geen meldingen gestuurd)"
enable_mentions: "Gebruikers mogen andere gebruikers noemen."
create_thumbnails: "Miniaturen maken voor lightbox-afbeeldingen die te groot zijn om in een bericht te passen."
email_time_window_mins: "(n) minuten wachten met het verzenden van meldingen per e-mail, zodat gebruikers de kans hebben om hun berichten te bewerken en te voltooien."
@@ -1555,7 +1554,7 @@ nl:
max_image_size_kb: "De maximale grootte van te uploaden afbeeldingen in kB. Dit moet ook worden geconfigureerd in nginx (client_max_body_size) / apache of proxy. Afbeeldingen die groter zijn dan deze waarde en kleiner dan client_max_body_size worden bij het uploaden verkleind."
max_attachment_size_kb: "De maximale grootte van te uploaden bijlagen in kB. Dit moet ook worden geconfigureerd in nginx (client_max_body_size) / apache of proxy."
authorized_extensions: "Een lijst van toegestane bestandsextensies voor uploaden (gebruik '*' voor alle bestandstypen)"
- authorized_extensions_for_staff: "Een lijst van toegestane bestandsextensies voor uploaden voor stafleden, naast de lijst gedefinieerd in de website-instelling `authorized_extensions` (gebruik '*' voor alle bestandstypen)"
+ authorized_extensions_for_staff: "Een lijst van toegestane bestandsextensies voor uploaden voor stafleden, naast de lijst gedefinieerd in de website-instelling 'authorized_extensions' (gebruik '*' voor alle bestandstypen)"
theme_authorized_extensions: "Een lijst van toegestane bestandsextensies voor thema-uploads (gebruik '*' voor alle bestandstypen)"
max_similar_results: "Hoeveel vergelijkbare topics om boven de editor te tonen bij het opstellen van een nieuw topic. Vergelijking is gebaseerd op titel en inhoud."
max_image_megapixels: "Maximale aantal toegestane megapixels voor een afbeelding. Afbeeldingen met een groter aantal megapixels worden geweigerd."
@@ -1577,9 +1576,9 @@ nl:
faq_url: "Als u ergens anders een FAQ hebt gehost die u wilt gebruiken, geef hier dan de volledige URL op."
tos_url: "Als u ergens anders een document met Algemene voorwaarden hebt gehost, geef hier dan de volledige URL op."
privacy_policy_url: "Als u ergens anders een document met een Privacybeleid hebt gehost, geef hier dan de volledige URL op."
- log_anonymizer_details: "Of de details van een gebruiker in het logboek worden behouden nadat ze zijn geanonimiseerd. Om aan de GDPR te voldoen, dient u dit uit te schakelen."
- newuser_spam_host_threshold: "Hoe vaak een nieuwe gebruiker een koppeling naar dezelfde host kan plaatsen binnen de `newuser_spam_host_threshold` berichten ervan voordat deze als spam worden beschouwd."
- allowed_spam_host_domains: "Een lijst met domeinen die niet op spam worden gecontroleerd. Nieuwe gebruikers kunnen onbeperkt koppelingen naar deze domeinen plaatsen."
+ log_anonymizer_details: "Of de gegevens van een gebruiker in de log worden behouden nadat ze zijn geanonimiseerd. Om aan de AVG te voldoen, moet dit worden uitgeschakeld."
+ newuser_spam_host_threshold: "Hoe vaak een nieuwe gebruiker een link naar dezelfde host kan plaatsen binnen de 'newuser_spam_host_threshold' berichten ervan voordat deze als spam wordt beschouwd."
+ allowed_spam_host_domains: "Een lijst met domeinen die niet op spam worden gecontroleerd. Nieuwe gebruikers kunnen onbeperkt links naar deze domeinen plaatsen."
topic_view_duration_hours: "Elke N uur één keer per IP/Gebruiker een nieuw-topicweergave tellen."
user_profile_view_duration_hours: "Elke N uur één keer per IP/Gebruiker een nieuw-gebruikersprofielweergave tellen."
levenshtein_distance_spammer_emails: "Bij het vergelijken van spam-e-mails, het aantal verschillende tekens waarbij nog steeds een wazige overeenkomst kan bestaan."
@@ -1592,7 +1591,7 @@ nl:
min_first_post_typing_time: "Minimale tijd in milliseconden dat een gebruiker moet typen tijdens een eerste bericht. Als de drempelwaarde niet wordt bereikt, wordt het bericht automatisch in de wachtrij voor goedkeuring gezet. Stel dit in op 0 om uit te schakelen (niet aanbevolen)."
auto_silence_fast_typers_on_first_post: "Automatisch gebruikers dempen die niet aan min_first_post_typing_time voldoen"
auto_silence_fast_typers_max_trust_level: "Maximale vertrouwensniveau om snelle typers automatisch te dempen"
- reviewable_claiming: "Dient beoordeelbare inhoud te worden opgeëist voordat er een handeling op kan worden uitgevoerd?"
+ reviewable_claiming: "Moet beoordeelbare inhoud worden geclaimd voordat er een actie op kan worden uitgevoerd?"
reviewable_default_topics: "Beoordeelbare inhoud standaard gegroepeerd op topic tonen"
reviewable_default_visibility: "Geen beoordeelbare items tonen, tenzij ze aan deze prioriteit voldoen"
high_trust_flaggers_auto_hide_posts: "Berichten van nieuwe gebruikers worden automatisch verborgen nadat ze door een TL3+-gebruiker als spam zijn gemarkeerd"
@@ -1606,9 +1605,9 @@ nl:
strip_images_from_short_emails: "Afbeeldingen met grootte van minder dan 2800 bytes uit e-mails verwijderen"
short_email_length: "Lengte van korte e-mail in bytes"
display_name_on_email_from: "Volledige namen in van-veld van e-mails weergeven"
- unsubscribe_via_email: "Gebruikers mogen zich uitschrijven van e-mails door een e-mail met 'unsubscribe' in het onderwerp of de tekst te sturen"
- unsubscribe_via_email_footer: "Een mailto:-koppeling voor uitschrijven via e-mail in de voettekst van verstuurde e-mails toevoegen"
- delete_email_logs_after_days: "E-maillogboeken na (N) dagen verwijderen. 0 voor oneindig behouden."
+ unsubscribe_via_email: "Sta gebruikers toe zich af te melden voor e-mails door een e-mail met 'afmelden' in het onderwerp of de tekst te sturen"
+ unsubscribe_via_email_footer: "Een mailto:-link voor afmelden via e-mail toevoegen in de voettekst van verzonden e-mails"
+ delete_email_logs_after_days: "E-maillogs verwijderen na (N) dagen. 0 voor oneindig bewaren."
disallow_reply_by_email_after_days: "Antwoord via e-mail na (N) dagen niet toestaan. 0 voor oneindig behouden."
max_emails_per_day_per_user: "Maximale aantal e-mails dat per dag naar gebruikers wordt verzonden. 0 om de limiet uit te schakelen"
enable_staged_users: "Automatisch staged gebruikers aanmaken bij het verwerken van inkomende e-mails."
@@ -1651,7 +1650,7 @@ nl:
imap_polling_old_emails: "Het maximale aantal oude e-mails (verwerkt) dat telkens bij het pollen van een IMAP-postvak moet worden bijgewerkt (0 voor alle)."
imap_polling_new_emails: "Het maximale aantal nieuwe e-mails (onverwerkt) dat telkens bij het pollen van een IMAP-postvak moet worden bijgewerkt."
imap_batch_import_email: "Het minimale aantal nieuwe e-mails dat de importmodus activeert (schakelt berichtmeldingen uit)."
- email_prefix: "Het [label] dat in het onderwerp van e-mails wordt gebruikt. Als niets is ingevuld, wordt 'title' gebruikt."
+ email_prefix: "Het [label] dat in het onderwerp van e-mails wordt gebruikt. Als niets is ingevuld, wordt 'titel' gebruikt."
email_site_title: "De titel van de website die als de afzender van e-mails van de website wordt gebruikt. Standaard wordt 'titel' gebruikt als niets is ingesteld. Gebruik deze instelling als uw 'titel' tekens bevat die niet in tekenreeksen van e-mailafzenders zijn toegestaan."
find_related_post_with_key: "Alleen de 'reply key' gebruiken om het beantwoorde bericht te vinden. WAARSCHUWING: uitschakelen hiervan staat imitatie van gebruikers op basis van e-mailadres toe."
minimum_topics_similar: "Het aantal topics dat moet bestaan voordat er vergelijkbare topics worden voorgesteld bij het opstellen van nieuwe topics."
@@ -1675,7 +1674,7 @@ nl:
apply_custom_styles_to_digest: "Aangepaste e-mailsjabloon en CSS worden op e-mailsamenvattingen toegepast."
email_accent_bg_color: "De te gebruiken accentkleur voor de achtergrond van bepaalde elementen in HTML-e-mails. Voer een kleurnaam ('red') of hex-waarde ('#FF0000') in."
email_accent_fg_color: "De kleur van gerenderde tekst op de achtergrondkleur van HTML-e-mails. Voer een kleurnaam ('white') of hex-waarde ('#FFFFFF') in."
- email_link_color: "De kleur van koppelingen in HTML-e-mails. Voer een kleurnaam ('blue') of hex-waarde ('#0000FF') in."
+ email_link_color: "De kleur van links in HTML-e-mails. Voer een kleurnaam ('blue') of hexwaarde ('#0000FF') in."
detect_custom_avatars: "Wel of niet te verifiëren of gebruikers eigen profielfoto's hebben geüpload."
max_daily_gravatar_crawls: "Maximale aantal keren dat Discourse op een dag bij Gravatar op aangepaste gravatars controleert"
public_user_custom_fields: "Een lijst van aangepaste gebruikersvelden die met de API kan worden opgehaald."
@@ -1683,16 +1682,16 @@ nl:
enable_user_directory: "Een lijst van gebruikers aanbieden om door te bladeren"
enable_group_directory: "Een lijst van groepen aanbieden om door te bladeren"
enable_category_group_moderation: "Groepen mogen inhoud in bepaalde categorieën modereren"
- group_in_subject: "%%{optional_pm} in e-mailonderwerp instellen op naam van eerste groep in PM; zie Customize subject format for standard emails"
+ group_in_subject: "Stel %%{optional_pm} in het e-mailonderwerp in op de naam van de eerste groep in PM; zie Onderwerpnotatie aanpassen voor standaard e-mails"
allow_anonymous_posting: "Gebruikers mogen naar anonieme modus overschakelen"
anonymous_posting_min_trust_level: "Het minimale vertrouwensniveau dat nodig is om anonieme berichtplaatsing in te schakelen"
anonymous_account_duration_minutes: "Om anonimiteit te beschermen, elke N minuten voor iedere gebruiker een nieuwe anonieme account aanmaken. Voorbeeld: als dit is ingesteld op 600, wordt een nieuwe anonieme account aangemaakt als er 600 minuten zijn verstreken na het laatste bericht EN de gebruiker naar anon overschakelt."
hide_user_profiles_from_public: "Gebruikerskaarten, gebruikersprofielen en gebruikerslijst voor anonieme gebruikers uitschakelen."
allow_users_to_hide_profile: "Gebruikers mogen hun profiel en aanwezigheid verbergen"
- allow_featured_topic_on_user_profiles: "Gebruikers mogen een koppeling naar een topic aanbevelen op hun gebruikerskaart en profiel."
+ allow_featured_topic_on_user_profiles: "Sta gebruikers toe een link naar een topic uit te lichten op hun gebruikerskaart en profiel."
show_inactive_accounts: "Aangemelde gebruikers mogen profielen van inactieve accounts doorbladeren."
hide_suspension_reasons: "Schorsingsredenen niet openbaar weergeven op gebruikersprofielen."
- log_personal_messages_views: "PM-weergaven door Admin voor andere gebruikers/groepen in logboek opslaan."
+ log_personal_messages_views: "PB-weergaven door beheerder voor andere gebruikers/groepen loggen."
ignored_users_count_message_threshold: "Moderators inlichten als een bepaalde gebruiker door dit aantal andere gebruikers wordt genegeerd."
ignored_users_message_gap_days: "Wachttijd voordat moderators opnieuw worden ingelicht over een gebruiker die door veel andere wordt genegeerd."
clean_up_inactive_users_after_days: "Aantal dagen voordat een inactieve gebruiker (vertrouwensniveau 0 zonder berichten) wordt verwijderd. Stel 0 in om opschonen uit te schakelen."
@@ -1701,7 +1700,7 @@ nl:
max_notifications_per_user: "Maximale aantal meldingen per gebruiker. Als dit aantal wordt overschreden, worden oude meldingen verwijderd. Wekelijks afgedwongen. Stel dit in op 0 om uit te schakelen."
allowed_user_website_domains: "Website van gebruiker wordt op deze domeinen gecontroleerd. Door pipes gescheiden lijst."
allow_profile_backgrounds: "Gebruikers mogen profielachtergronden uploaden."
- sequential_replies_threshold: "Aantal berichten dat een gebruiker achter elkaar in één topic moet plaatsen voordat deze aan te veel opeenvolgende reacties wordt herinnerd."
+ sequential_replies_threshold: "Aantal berichten dat een gebruiker achter elkaar in één topic moet plaatsen voordat deze aan te veel opeenvolgende antwoorden wordt herinnerd."
get_a_room_threshold: "Aantal berichten dat een gebruiker aan dezelfde persoon in hetzelfde topic moet richten voordat deze wordt gewaarschuwd."
enable_mobile_theme: "Mobiele apparaten gebruiken een mobielvriendelijk thema, met de mogelijkheid om naar de volledige website over te schakelen. Schakel dit uit als u een eigen stylesheet wilt gebruiken dat volledig responsief is."
dominating_topic_minimum_percent: "Het percentage berichten dat een gebruiker in een topic moet maken voordat deze aan het te veel domineren van een topic wordt herinnerd."
@@ -1709,10 +1708,10 @@ nl:
suppress_uncategorized_badge: "De badge voor ongecategoriseerde topics niet in topiclijsten tonen."
header_dropdown_category_count: "Het aantal categorieën dat in het header-vervolgkeuzemenu kan worden weergegeven."
permalink_normalizations: "De volgende reguliere expressie toepassen voordat permalinks worden verwerkt. Voorbeeld: /(topic.*)\\?.*/\\1 verwijdert querystrings uit topicroutes. Notatie is regex+strings, gebruik \\1 etc. voor deeluitdrukkingen."
- global_notice: "Een algemene niet te verbergen DRINGEND, NOODGEVAL-bannermelding voor alle gebruikers weergeven. Laat leeg om deze te verbergen (HTML toegestaan)."
+ global_notice: "Toon een algemene niet te sluiten DRINGENDE NOOD-bannermelding voor alle gebruikers. Laat dit leeg om te verbergen (HTML toegestaan)."
disable_system_edit_notifications: "Schakelt bewerkingsmeldingen van de systeemgebruiker uit als 'download_remote_images_to_local' actief is."
notification_consolidation_threshold: "Aantal ontvangen meldingen van likes of lidmaatschapsaanvragen voordat de meldingen in één melding worden samengevoegd. Stel dit in op 0 om uit te schakelen."
- likes_notification_consolidation_window_mins: "Tijdsduur in minuten waarin like-meldingen in één melding worden samengevoegd zodra de drempel is bereikt. De drempel kan worden geconfigureerd via `SiteSetting.notification_consolidation_threshold`."
+ likes_notification_consolidation_window_mins: "Tijdsduur in minuten waarin like-meldingen in één melding worden samengevoegd zodra de drempel is bereikt. De drempel kan worden geconfigureerd via 'SiteSetting.notification_consolidation_threshold'."
automatically_unpin_topics: "Topics automatisch losmaken wanneer de gebruiker de onderkant bereikt."
read_time_word_count: "Aantal woorden per minuut voor het berekenen van geschatte leestijd."
topic_page_title_includes_category: "Topicpagina titeltag bevat de categorienaam."
@@ -1738,7 +1737,7 @@ nl:
embed_unlisted: "Geïmporteerde topics zijn onzichtbaar totdat een gebruiker antwoordt."
embed_support_markdown: "Markdown-opmaak voor ingebedde berichten ondersteunen."
allowed_embed_selectors: "Een door komma's gescheiden lijst van CSS-elementen die bij inbedding zijn toegestaan."
- allowed_href_schemes: "Toegestane schema's in koppelingen, naast http en https."
+ allowed_href_schemes: "Toegestane schema's in links, naast http en https."
embed_post_limit: "Maximale aantal in te bedden berichten."
embed_username_required: "De gebruikersnaam voor het maken van topics is vereist."
notify_about_flags_after: "Als er na dit aantal uren markeringen zijn die nog niet zijn afgehandeld, een privébericht naar stafleden sturen. Stel dit in op 0 om uit te schakelen."
@@ -1758,7 +1757,7 @@ nl:
notify_about_queued_posts_after: "Als er berichten zijn die meer dan dit aantal uren op beoordeling wachten, een melding naar alle moderators sturen. Stel dit in op 0 om deze meldingen uit te schakelen."
auto_close_messages_post_count: "Maximale aantal toegestane berichten in een bericht voordat het automatisch wordt gesloten (0 voor uitschakelen)"
auto_close_topics_post_count: "Maximale aantal toegestane berichten in een topic voordat het automatisch wordt gesloten (0 voor uitschakelen)"
- auto_close_topics_create_linked_topic: "Een nieuw gekoppeld topic aanmaken wanneer een onderwerp automatisch wordt gesloten op basis van de instelling 'auto close topics post count'"
+ auto_close_topics_create_linked_topic: "Een nieuw gekoppeld topic aanmaken wanneer een topic automatisch wordt gesloten op basis van de instelling 'auto close topics post count'"
code_formatting_style: "Codeknop in editor gebruikt standaard deze stijl voor codeopmaak"
max_allowed_message_recipients: "Maximale aantal ontvangers in een bericht."
watched_words_regular_expressions: "In de gaten gehouden woorden zijn reguliere expressies."
@@ -1780,27 +1779,27 @@ nl:
default_other_new_topic_duration_minutes: "Globale standaardvoorwaarde waarvoor een topic als nieuw wordt beschouwd."
default_other_auto_track_topics_after_msecs: "Globale standaardtijd voordat een topic automatisch wordt gevolgd."
default_other_notification_level_when_replying: "Globale standaard meldingsniveau wanneer de gebruiker op een topic antwoordt."
- default_other_external_links_in_new_tab: "Externe koppelingen standaard in een nieuw tabblad openen."
+ default_other_external_links_in_new_tab: "Externe links standaard in een nieuw tabblad openen."
default_other_enable_quoting: "Antwoord-met-citaat voor gemarkeerde tekst standaard inschakelen."
default_other_enable_defer: "Topicnegeerfunctionaliteit standaard inschakelen."
default_other_dynamic_favicon: "Aantal nieuwe / bijgewerkte topics standaard tonen op browserpictogram."
default_other_skip_new_user_tips: "Onboarding-tips en -badges voor nieuwe gebruikers overslaan."
default_other_like_notification_frequency: "Gebruikers standaard informeren bij likes"
default_topics_automatic_unpin: "Topics standaard automatisch losmaken wanneer de gebruiker de onderkant bereikt."
- default_categories_watching: "Lijst van categorieën die standaard in de gaten worden gehouden."
+ default_categories_watching: "Lijst van categorieën die standaard worden geobserveerd."
default_categories_tracking: "Lijst van categorieën die standaard worden gevolgd."
default_categories_muted: "Lijst van categorieën die standaard worden gedempt."
- default_categories_watching_first_post: "Lijst van categorieën waarin het eerste bericht in elk nieuw topic standaard in de gaten wordt gehouden."
+ default_categories_watching_first_post: "Lijst van categorieën waarin het eerste bericht in elk nieuw topic standaard wordt geobserveerd."
default_categories_normal: "Lijst van categorieën die niet standaard worden gedempt. Handig wanneer de website-instelling 'mute_all_categories_by_default' is ingeschakeld."
- mute_all_categories_by_default: "Het standaard meldingsniveau van alle categorieën instellen op gedempt. Gebruikers moeten zich bij categorieën aanmelden om deze in de pagina's 'nieuwste' en 'categorieën' te laten verschijnen. Als u de standaardwaarden voor anonieme gebruikers wilt wijzigen, stel dan de instellingen voor 'default_categories_' in."
- default_tags_watching: "Lijst van tags die standaard in de gaten worden gehouden."
+ mute_all_categories_by_default: "Stel het standaard meldingsniveau van alle categorieën in op gedempt. Gebruikers moeten zich aanmelden voor categorieën om deze op de pagina's 'Nieuwste' en 'Categorieën' te laten weergeven. Stel de instellingen voor 'default_categories_' in om de standaardwaarden voor anonieme gebruikers te wijzigen."
+ default_tags_watching: "Lijst van tags die standaard worden geobserveerd."
default_tags_tracking: "Lijst van tags die standaard worden gevolgd."
default_tags_muted: "Lijst van tags die standaard worden gedempt."
- default_tags_watching_first_post: "Lijst van tags waarin het eerste bericht in elk nieuw topic standaard in de gaten wordt gehouden."
+ default_tags_watching_first_post: "Lijst van tags waarin het eerste bericht in elk nieuw topic standaard wordt geobserveerd."
default_text_size: "Tekstgrootte die standaard is geselecteerd"
default_title_count_mode: "Standaardmodus voor de paginatitelteller"
retain_web_hook_events_period_days: "Aantal dagen voor het behouden van records van webhookgebeurtenissen."
- retry_web_hook_events: "Mislukte webhookgebeurtenissen automatisch 4 keer opnieuw proberen. Tijdsgaten tussen de pogingen zijn 1, 5, 25 en 125 minuten."
+ retry_web_hook_events: "Mislukte webhookgebeurtenissen automatisch 4 keer opnieuw proberen. De intervallen tussen de pogingen zijn 1, 5, 25 en 125 minuten."
revoke_api_keys_days: "Aantal dagen voordat een niet-gebruikte API-sleutel van een gebruiker automatisch wordt ingetrokken (0 voor nooit)"
allow_user_api_keys: "Genereren van API-sleutels van gebruiker toestaan"
allow_user_api_key_scopes: "Lijst van toegestane scopes voor API-sleutels van gebruiker"
@@ -1827,7 +1826,7 @@ nl:
shared_drafts_category: "Schakel de functie Gedeelde concepten in door een categorie voor topicconcepten aan te geven. Topics in deze categorie worden onderdrukt in topiclijsten voor stafgebruikers."
shared_drafts_min_trust_level: "Gebruikers mogen gedeelde concepten zien en bewerken."
push_notifications_prompt: "Prompt voor gebruikerstoestemming weergeven."
- push_notifications_icon: "Het badgepictogram dat in de meldingshoek verschijnt. Een eenkleurige PNG van 96×96 met transparantie wordt aanbevolen."
+ push_notifications_icon: "Het badgepictogram dat in de meldingshoek wordt weergegeven. Een eenkleurige PNG van 96×96 met transparantie wordt aanbevolen."
heading_font: "Te gebruiken lettertypen voor kopteksten op de website. Thema's kunnen worden overschreven via de aangepaste CSS-eigenschap '--heading-font-family'."
short_title: "De korte titel wordt gebruikt op het startscherm van de gebruiker, de starter, of andere plaatsen waar ruimte beperkt kan zijn. Beperk de naam tot 12 tekens."
dashboard_hidden_reports: "Toestaan dat de opgegeven rapporten worden verborgen op het dashboard."
@@ -1853,24 +1852,24 @@ nl:
invalid_json: "Ongeldige JSON."
invalid_reply_by_email_address: "Waarde moet '%{reply_key}' bevatten en afwijken van de melding per e-mail."
invalid_alternative_reply_by_email_addresses: "Alle waarden moeten '%{reply_key}' bevatten en afwijken van de melding per e-mail."
- pop3_polling_host_is_empty: "U moet een 'pop3 polling host' instellen voordat u POP3-polling inschakelt."
- pop3_polling_username_is_empty: "U moet een 'pop3 polling username' instellen voordat u POP3-polling inschakelt."
- pop3_polling_password_is_empty: "U moet een 'pop3 polling password' instellen voordat u POP3-polling inschakelt."
+ pop3_polling_host_is_empty: "Je moet een 'pop3 polling host' instellen voordat je POP3-polling inschakelt."
+ pop3_polling_username_is_empty: "Je moet een 'pop3 polling username' instellen voordat je POP3-polling inschakelt."
+ pop3_polling_password_is_empty: "Je moet een 'pop3 polling password' instellen voordat je POP3-polling inschakelt."
pop3_polling_authentication_failed: "POP3-authenticatie mislukt. Verifieer uw POP3-referenties."
- reply_by_email_address_is_empty: "U moet een 'reply by email address' instellen voordat u antwoorden per e-mail inschakelt."
- email_polling_disabled: "U moet handmatige of POP3-polling inschakelen voordat u antwoorden per e-mail inschakelt."
- user_locale_not_enabled: "U moet eerst 'allow user locale' inschakelen voordat u deze instelling inschakelt."
+ reply_by_email_address_is_empty: "Je moet een 'reply by email address' instellen voordat je antwoorden via e-mail inschakelt."
+ email_polling_disabled: "Je moet handmatige of POP3-polling inschakelen voordat je antwoorden via e-mail inschakelt."
+ user_locale_not_enabled: "Je moet 'allow user locale' inschakelen voordat je deze instelling inschakelt."
invalid_regex: "Regex is ongeldig of niet toegestaan."
- email_editable_enabled: "U moet 'email editable' uitschakelen voordat u deze instelling inschakelt."
- staged_users_disabled: "U moet eerst 'staged users' inschakelen voordat u deze instelling inschakelt."
- reply_by_email_disabled: "U moet eerst 'reply by email' inschakelen voordat u deze instelling inschakelt."
- enable_local_logins_disabled: "U moet eerst 'enable local logins' inschakelen voordat u deze instelling inschakelt."
- min_username_length_exists: "U kunt de minimale gebruikersnaamlengte niet hoger instellen dan de kortste gebruikersnaam (%{username})."
- min_username_length_range: "U kunt het minimum niet hoger instellen dan het maximum."
- max_username_length_exists: "U kunt de maximale gebruikersnaamlengte niet lager instellen dan de langste gebruikersnaam (%{username})."
- max_username_length_range: "U kunt het maximum niet lager instellen dan het minimum."
+ email_editable_enabled: "Je moet 'email editable' uitschakelen voordat je deze instelling inschakelt."
+ staged_users_disabled: "Je moet eerst 'staged users' inschakelen voordat je deze instelling inschakelt."
+ reply_by_email_disabled: "Je moet eerst 'reply by email' inschakelen voordat je deze instelling inschakelt."
+ enable_local_logins_disabled: "Je moet eerst 'enable local logins' inschakelen voordat je deze instelling inschakelt."
+ min_username_length_exists: "Je kunt de minimale gebruikersnaamlengte niet hoger instellen dan de kortste gebruikersnaam (%{username})."
+ min_username_length_range: "Je kunt het minimum niet hoger instellen dan het maximum."
+ max_username_length_exists: "Je kunt de maximale gebruikersnaamlengte niet lager instellen dan de langste gebruikersnaam (%{username})."
+ max_username_length_range: "Je kunt het maximum niet lager instellen dan het minimum."
invalid_hex_value: "Kleurwaarden moeten 6-cijferige hexadecimale codes zijn."
- empty_selectable_avatars: "U moet eerst minstens twee selecteerbare avatars uploaden voordat u deze instelling inschakelt."
+ empty_selectable_avatars: "Je moet eerst minimaal twee selecteerbare avatars uploaden voordat je deze instelling inschakelt."
allowed_unicode_usernames:
regex_invalid: "De reguliere expressie is ongeldig: %{error}"
leading_trailing_slash: "De reguliere expressie mag niet met een schuine streep beginnen en eindigen."
@@ -1893,19 +1892,19 @@ nl:
discourse_connect:
login_error: "Aanmeldingsfout"
not_found: "Uw account kon niet worden gevonden. Neem contact op met de beheerder van de website."
- account_not_approved: "Uw account wacht op goedkeuring. U ontvangt een e-mailmelding zodra deze is goedgekeurd."
+ account_not_approved: "Je account wacht op goedkeuring. Je ontvangt een e-mailmelding zodra het is goedgekeurd."
unknown_error: "Er is een probleem met uw account. Neem contact op met de beheerder van de website."
timeout_expired: "Time-out bij accountaanmelding, probeer u opnieuw aan te melden."
no_email: "Er is geen e-mailadres opgegeven. Neem contact op met de beheerder van de website."
- blank_id_error: "De `external_id` is vereist, maar was leeg"
+ blank_id_error: "De 'external_id' is vereist, maar was leeg"
email_error: "Er kon geen account met het e-mailadres %{email} worden geregistreerd. Neem contact op met de beheerder van de website."
original_poster: "Oorspronkelijk geplaatst door"
most_recent_poster: "Meest recente schrijver"
frequent_poster: "Frequente schrijver"
poster_description_joiner: ", "
redirected_to_top_reasons:
- new_user: "Welkom bij onze gemeenschap! Dit zijn de meest populaire recente topics."
- not_seen_in_a_month: "Welkom terug! We hebben u een tijdje niet gezien. Dit zijn de meest populaire topics sinds uw afwezigheid."
+ new_user: "Welkom bij onze community! Dit zijn de meest populaire recente topics."
+ not_seen_in_a_month: "Welkom terug! We hebben je een tijdje niet gezien. Dit zijn de meest populaire topics sinds je afwezigheid."
merge_posts:
edit_reason:
one: "Een bericht is samengevoegd door %{username}"
@@ -1977,7 +1976,7 @@ nl:
one: "Dit topic is %{count} minuut na het laatste antwoord automatisch geopend."
other: "Dit topic is %{count} minuten na het laatste antwoord automatisch geopend."
autoclosed_disabled: "Dit topic is nu geopend. Nieuwe antwoorden zijn toegestaan."
- autoclosed_disabled_lastpost: "Dit topic is nu geopend. Nieuwe reacties worden weer geaccepteerd."
+ autoclosed_disabled_lastpost: "Dit topic is nu geopend. Nieuwe antwoorden zijn toegestaan."
auto_deleted_by_timer: "Automatisch verwijderd door timer."
login:
security_key_description: "Houd uw fysieke beveiligingssleutel gereed en klik op de onderstaande knop Authenticeren met beveiligingssleutel."
@@ -1987,36 +1986,36 @@ nl:
security_key_no_matching_credential_error: "Geen referenties gevonden in de opgegeven beveiligingssleutel."
security_key_support_missing_error: "Uw huidige apparaat of browser ondersteunt geen gebruik van beveiligingssleutels. Gebruik een andere methode."
security_key_invalid: "Er is een fout opgetreden bij het valideren van de beveiligingssleutel."
- not_approved: "Uw account is nog niet goedgekeurd. U ontvangt een melding via e-mail zodra u zich kunt aanmelden."
+ not_approved: "Je account is nog niet goedgekeurd. Je ontvangt een melding via e-mail zodra je je kunt aanmelden."
incorrect_username_email_or_password: "Onjuiste gebruikersnaam, e-mailadres of wachtwoord"
incorrect_password: "Onjuist wachtwoord"
- wait_approval: "Bedankt voor het registreren. We laten u weten wanneer uw account is goedgekeurd."
+ wait_approval: "Bedankt voor het registreren. We laten het je weten wanneer je account is goedgekeurd."
active: "Uw account is geactiveerd en gereed voor gebruik."
activate_email: "
Divirta-se!"
styling:
- title: "Aparência & Comportamento"
+ title: "Aparência e Comportamento"
fields:
color_scheme:
label: "Esquema de cores"
@@ -4506,7 +4503,7 @@ pt_BR:
categories_boxes_with_topics:
label: "Caixas de Categorias com Tópicos"
subcategories_with_featured_topics:
- label: "Sub-Categorias com tópicos em destaque"
+ label: "Subcategorias com tópicos em destaque"
branding:
title: "Personalizar logos"
fields:
diff --git a/config/locales/server.ro.yml b/config/locales/server.ro.yml
index 086aeed9eb..a1658ae85d 100644
--- a/config/locales/server.ro.yml
+++ b/config/locales/server.ro.yml
@@ -452,7 +452,6 @@ ro:
few: "cu aproape %{count} ani în urmă"
other: "cu aproape %{count} de ani în urmă"
password_reset:
- no_token: "Ne pare rău, dar link-ul de schimbare a parolei este prea vechi. Selectează butonul Înregistrare și folosește 'Mi-am uitat parola' pentru a obține un nou link."
choose_new: "Alege o parolă nouă"
choose: "Alege o parolă"
update: "Actualizează parola"
diff --git a/config/locales/server.ru.yml b/config/locales/server.ru.yml
index 70478903ff..b01b503e08 100644
--- a/config/locales/server.ru.yml
+++ b/config/locales/server.ru.yml
@@ -546,7 +546,6 @@ ru:
Вместо нового ответа, можно начать редактировать свой предыдущий ответ и просто добавить в него новые цитаты и ответы на них. Для этого нужно выделить требуемый текст и нажать на появившуюся кнопку Ответить с цитированием.
Для большинства людей намного проще читать темы, содержащие несколько ответов в одном сообщении, чем много отдельных коротких ответов.
- dominating_topic: Вы опубликовали здесь более %{percent}% ответов, возможно, стоит услышать ещё чьё-то мнение?
get_a_room: Вы ответили пользователю @%{reply_username} %{count} раза; знаете ли вы, что вместо этого ему можно отправить личное сообщение?
too_many_replies: |
### Вы дали максимально возможное количество ответов в этой теме.
@@ -867,7 +866,6 @@ ru:
many: "почти %{count} лет назад"
other: "почти %{count} лет назад"
password_reset:
- no_token: "К сожалению, ссылка на изменение пароля устарела. Нажмите на кнопку \"Войти\", а затем на \"Я забыл свой пароль\", чтобы создать новую ссылку для изменения пароля."
choose_new: "Введите новый пароль"
choose: "Введите пароль"
update: "Обновить пароль"
@@ -3859,7 +3857,6 @@ ru:
png_to_jpg_conversion_failure_message: "Произошла ошибка при конвертации из PNG в JPG."
optimize_failure_message: "При оптимизации загруженного изображения произошла ошибка."
download_failure: "Не удалось загрузить файл от внешнего провайдера."
- size_mismatch_failure: "Размер файла, загруженного в S3, не совпадает с предполагаемым размером файла. %{additional_detail}"
create_multipart_failure: "Не удалось создать многокомпонентную загрузку во внешнем хранилище."
abort_multipart_failure: "Не удалось прервать многокомпонентную загрузку во внешнем хранилище."
complete_multipart_failure: "Не удалось выполнить многокомпонентную загрузку во внешнем хранилище."
diff --git a/config/locales/server.sk.yml b/config/locales/server.sk.yml
index a47d22f741..4fcd1fdc6a 100644
--- a/config/locales/server.sk.yml
+++ b/config/locales/server.sk.yml
@@ -463,7 +463,6 @@ sk:
many: "pred takmer %{count} rokmi"
other: "pred takmer %{count} rokmi"
password_reset:
- no_token: "Ľutujeme, tento odkaz na zmenu hesla je už príliš starý. Stlačte tlačidlo Prihlásiť a použite \"Zabudol som heslo\" pre vytvorenie nového odkazu."
choose_new: "Napíš nové heslo"
choose: "Napíš heslo"
update: "Aktualizujte heslo"
diff --git a/config/locales/server.sl.yml b/config/locales/server.sl.yml
index 2d2d5f618d..f922d398af 100644
--- a/config/locales/server.sl.yml
+++ b/config/locales/server.sl.yml
@@ -458,7 +458,6 @@ sl:
few: "skoraj %{count} leta nazaj"
other: "skoraj %{count} let nazaj"
password_reset:
- no_token: "Povezava za zamenjavo gesla je potekla. Izberite gumb Prijava in uporabite 'Pozabil sem geslo' da dobite novo povezavo.."
choose_new: "Vnesite novo geslo"
choose: "Vnesite geslo"
update: "Spremenite geslo"
diff --git a/config/locales/server.sq.yml b/config/locales/server.sq.yml
index aa8cb90b12..81f20578c0 100644
--- a/config/locales/server.sq.yml
+++ b/config/locales/server.sq.yml
@@ -300,7 +300,6 @@ sq:
one: "pothuajse %{count} vit më parë"
other: "pothuajse %{count} vite më parë"
password_reset:
- no_token: "Ndjesë, kjo lidhje për ndryshimin e fjalëkalimit është shumë e vjetër. Kliko butonin 'Identifikohu' dhe përdorni 'Kam harruar fjalëkalimin' për të marrë një lidhje të re."
choose_new: "Zgjidhni një fjalëkalim të ri"
choose: "Zgjidhni një fjalëkalim të ri"
update: "Rifresko Fjalëkalimin"
diff --git a/config/locales/server.sv.yml b/config/locales/server.sv.yml
index 93c30bc340..05bf6a7acc 100644
--- a/config/locales/server.sv.yml
+++ b/config/locales/server.sv.yml
@@ -67,6 +67,7 @@ sv:
modifier_values: "about.json-modifierare innehåller ogiltiga värden: %{errors}"
git: "Fel vid kloning av git-lagringsplats, åtkomst nekas eller lagringsplatsen finns inte"
git_ref_not_found: "Det gick inte att ta ut git-referens: %{ref}"
+ git_unsupported_scheme: "Det går inte att klona git repo: schemat stöds inte"
unpack_failed: "Det gick inte att packa upp filen"
file_too_big: "Den okomprimerade filen är för stor."
unknown_file_type: "Filen som du laddade upp verkar inte vara ett giltigt Discourse-tema."
@@ -248,6 +249,7 @@ sv:
max_redemptions_allowed_one: "för e-postinbjudningar ska vara 1."
redemption_count_less_than_max: "bör vara mindre än %{max_redemptions_allowed}."
email_xor_domain: "E-post- och domänfält är inte tillåtna samtidigt"
+ existing_user_success: "Inbjudan har lösts in"
bulk_invite:
file_should_be_csv: "De uppladdade filerna skall vara i csv-format."
max_rows: "De första %{max_bulk_invites} inbjudningarna har skickats ut. Prova att dela upp filen i mindre delar."
@@ -495,7 +497,7 @@ sv:
Du kan redigera ditt tidigare svar och lägga till ett citat genom att markera text och välja knappen citera som dyker upp.
Det blir lättare för alla att läsa ämnen som har färre in-på-djupet-svar istället för flera små, individuella svar.
- dominating_topic: Du har publicerat mer än %{percent} % av svaren här. Finns det någon annan du vill höra från?
+ dominating_topic: Du har skrivit mer än %{percent} % av svaren här. Får vi föreslå att du ger andra en möjlighet att komma till tals?
get_a_room: Du har svarat @%{reply_username} %{count} gånger. Visste du att du istället kunde skicka ett personligt meddelande till henne/honom?
too_many_replies: |
### Du har nått svarsgränsen för detta ämne
@@ -764,7 +766,7 @@ sv:
one: "nästan %{count} år sedan"
other: "nästan %{count} år sedan"
password_reset:
- no_token: "Tyvärr har din lösenordslänk löpt ut. Klicka på inloggningsknappen och välj \"jag har glömt mitt lösenord\" för att få en ny länk."
+ no_token: 'Hoppsan! Länken du använde fungerar inte längre. Du kan logga in nu. Om du har glömt ditt lösenord kan du begära en länk för att återställa det.'
choose_new: "Välj ett nytt lösenord"
choose: "Välj ett lösenord"
update: "Uppdatera lösenord"
@@ -2049,6 +2051,7 @@ sv:
disable_mailing_list_mode: "Tillåt inte att användare aktiverar utskicksläget för e-postlistor (förhindrar att meddelanden till e-postlistan skickas.)"
default_email_previous_replies: "Som standard omfattas tidigare svar i e-postmeddelanden."
default_email_in_reply_to: "Omfatta ett utdrag av svar på inlägg i e-post som standard."
+ default_hide_profile_and_presence: "Dölj användarens offentliga profil och närvarofunktioner som standard."
default_other_new_topic_duration_minutes: "Generellt standardvillkor för när ett ämne anses nytt."
default_other_auto_track_topics_after_msecs: "Generell standardtid innan ett ämne följs automatiskt."
default_other_notification_level_when_replying: "Generell standardiserad bevakningssnivå när en användare svarar i ett ämne."
@@ -3517,7 +3520,7 @@ sv:
png_to_jpg_conversion_failure_message: "Ett fel uppstod vid konvertering från PNG till JPG."
optimize_failure_message: "Det uppstod ett fel vid optimering av den uppladdade bilden."
download_failure: "Det gick inte att hämta filen från den externa leverantören."
- size_mismatch_failure: "Storleken på filen som uppladdats till S3 matchade inte avsedd storlek på extern uppladdnings-stub. %{additional_detail}"
+ size_mismatch_failure: "Storleken på filen som uppladdats till S3 matchade inte avsedd storlek på den externa uppladdningen. %{additional_detail}"
create_multipart_failure: "Det gick inte att skapa flerdelad uppladdning i den externa butiken."
abort_multipart_failure: "Det gick inte att avbryta flerdelad uppladdning i den externa butiken."
complete_multipart_failure: "Det gick inte att slutföra flerdelad uppladdning i den externa butiken."
@@ -4376,6 +4379,10 @@ sv:
user_status:
errors:
ends_at_should_be_greater_than_set_at: "ends_at bör vara större än set_at."
+ webhooks:
+ payload_url:
+ blocked_or_internal: "Försändelse-URL:en kan inte användas eftersom den leder till en blockerad eller intern IP-adress."
+ unsafe: "Försändelse-URL:en kan inte användas eftersom den är osäker"
activemodel:
errors:
<<: *errors
diff --git a/config/locales/server.sw.yml b/config/locales/server.sw.yml
index 7d4f0afb92..1a87e5de67 100644
--- a/config/locales/server.sw.yml
+++ b/config/locales/server.sw.yml
@@ -406,7 +406,6 @@ sw:
one: "siku %{count} iliyopita"
other: "siku %{count} zilizopita"
password_reset:
- no_token: "Samahani, kiungo hicho cha kuingia kupitia barua pepe ni cha mda sana. Bonyeza kitufe cha Kuingia na tumia 'Nimesahau nywila' ili upate kiungo kipya."
choose_new: "Chagua nywila"
choose: "Chagua nywila"
update: "Sasisha Nywila"
diff --git a/config/locales/server.tr_TR.yml b/config/locales/server.tr_TR.yml
index ecaa79b49b..77cd0b4edb 100644
--- a/config/locales/server.tr_TR.yml
+++ b/config/locales/server.tr_TR.yml
@@ -484,7 +484,6 @@ tr_TR:
Herkesin benzersiz bir profil resmi olduğunda, tartışmaları takip etmek ve konuşmalarda ilginç insanlar bulmak daha kolay!
sequential_replies: "### Aynı anda birkaç gönderiye yanıt vermeyi denediniz \nBunun yerine yerine, lütfen önceki gönderilerden alıntılar veya @name referansları içeren tek bir yanıt veriniz.\nMetni vurgulayıp görünen alıntı düğmesini seçerek önceki yanıtınızı düzenleyebilirsiniz. Böylece diğer kullanıcıların, yanıtlarınızı okuması daha kolay olacaktır.\n"
- dominating_topic: Birden fazla gönderdiniz %{percent}Burada cevapların% else orada herkesin duymak isteriz edilir?
get_a_room: '@%{reply_username} %{count} kez yanıt verdiniz, bunun yerine ona kişisel bir ileti gönderebileceğinizi biliyor muydunuz?'
too_many_replies: |
### Bu konu için yanıt limitinizi doldurdunuz
@@ -746,7 +745,6 @@ tr_TR:
one: "neredeyse %{count} yıl önce"
other: "neredeyse %{count} yıl önce"
password_reset:
- no_token: "Üzgünüz, bu parola değiştirme bağlantısı çok eski. Yeni bir bağlantı almak için lütfen 'Giriş Yap' tuşuna basın ve 'Parolamı unuttum'u seçin."
choose_new: "Yeni bir parola seç"
choose: "Parola seç"
update: "Parolayı Güncelle"
diff --git a/config/locales/server.uk.yml b/config/locales/server.uk.yml
index 4aa4c51d26..116f0b74b2 100644
--- a/config/locales/server.uk.yml
+++ b/config/locales/server.uk.yml
@@ -539,7 +539,6 @@ uk:
Замість нового відповіді, зараз можна почати редагувати свій попередній відповідь і просто додати в нього нові цитати і відповіді та них. Для цього потрібно виділити необхідний текст і натиснути на кнопку, що з'явилася відповісти цитуванням.
Для більшості людей набагато простіше читати теми, у яких довгі відповіді і їх мало, ніж коли багато коротеньких відповідей.
- dominating_topic: Ви розмістили тут більше %{percent}% відповідей, можливо є ще хтось, кого варто було би почути?
get_a_room: Ви відповіли користувачу @%{reply_username} %{count} разів, ви знали, що можете надіслати їм особисте повідомлення?
too_many_replies: "### Ви досягли межі відповідей в цю тему. \nНа жаль, нові користувачі тимчасово обмежені %{newuser_max_replies_per_topic} відповідей в цій темі. \nЗамість того, щоб додавати ще одну відповідь, будь ласка, подумайте про редагування попередніх відповідей або відвідайте інші теми.\n"
reviving_old_topic: |
@@ -852,7 +851,6 @@ uk:
many: "майже %{count} років тому"
other: "майже %{count} років тому"
password_reset:
- no_token: "На жаль, дане посилання на зміну пароля застаріло. Натисніть на кнопку \"Увійти\", а потім на \"Я забув свій пароль\", щоб згенерувати нове посилання для зміни пароля."
choose_new: "Вибрати новий пароль"
choose: "Вибрати пароль"
update: "Оновити пароль"
diff --git a/config/locales/server.ur.yml b/config/locales/server.ur.yml
index 9fbc551756..c10603c329 100644
--- a/config/locales/server.ur.yml
+++ b/config/locales/server.ur.yml
@@ -480,7 +480,6 @@ ur:
متن کو اُجاگر کر کہ ظاہر ہونے والے جواب اقتباس کریں بٹن منتخب کرنے سے ایک اقتباس شامل کرنے کیلئے آپ اپنے پچھلے جواب میں ترمیم کرسکتے ہیں۔
ہر ایک کیلئے اُن ٹاپکس کو پڑھ نا زیادہ آسان ہے جس میں بہت سے چھوٹے، انفرادی جوابات کے مقابلے میں تفصیلی لیکن کم جوابات ہوں۔
- dominating_topic: آپ نے یہاں %{percent}% سے زیادہ جوابات پوسٹ کیے ہیں، کیا کوئی اور ہے جس سے آپ سننا چاہیں گے؟
get_a_room: آپ نے @%{reply_username} %{count} بار جواب دیا ہے، کیا آپ جانتے ہیں کہ اس کے بجائے آپ انہیں ذاتی پیغام بھیج سکتے ہیں؟
too_many_replies: |
### آپ اس ٹاپک پر جوابات کے نمبر کی حد تک پہنچ گئے ہیں
@@ -736,7 +735,6 @@ ur:
one: "تقریباً %{count} سال قبل"
other: "تقریباً %{count} سال قبل"
password_reset:
- no_token: "معذرت، وہ پاسورڈ تبدیل کرنے کا لِنک بہت پرانا ہے۔ لاگ ان کے بٹن کو منتخب کریں اور ایک نیا لِنک حاصل کرنے کیلئے 'میں اپنا پاسورڈ بھول گیا' کا استعمال کریں۔"
choose_new: "نیا پاسورڈ منتخب کریں"
choose: "پاسورڈ منتخب کریں"
update: "پاسورڈ اَپ ڈیٹ کریں"
diff --git a/config/locales/server.vi.yml b/config/locales/server.vi.yml
index 28c762e094..0ecd565086 100644
--- a/config/locales/server.vi.yml
+++ b/config/locales/server.vi.yml
@@ -602,7 +602,6 @@ vi:
almost_x_years:
other: "gần %{count} năm trước"
password_reset:
- no_token: "Xin lỗi, liên kết đổi mật khẩu đã cũ. Chọn \"Đăng nhập\" và sử dụng chức năng \"Quên mật khẩu\" để lấy liên kết mới."
choose_new: "Chọn một mật khẩu mới"
choose: "Chọn một mật khẩu"
update: "Cập nhật mật khẩu"
diff --git a/config/locales/server.zh_CN.yml b/config/locales/server.zh_CN.yml
index 8339bc4365..6110f77b06 100644
--- a/config/locales/server.zh_CN.yml
+++ b/config/locales/server.zh_CN.yml
@@ -470,7 +470,6 @@ zh_CN:
您可以突出显示文本并选择出现的引用回复按钮,对您之前的回复进行编辑以添加引用。
这样,每个人都可以更轻松地阅读具有少量嵌套回复的话题,而不是查看大量的个别回复。
- dominating_topic: 您在这里发布了超过 %{percent}% 的回复,想听听其他人的发言吗?
get_a_room: 您已回复 @%{reply_username} %{count} 次,您知道您可以向他们发送个人消息吗?
too_many_replies: |
### 您已达到此话题的回复上限
@@ -713,7 +712,6 @@ zh_CN:
almost_x_years:
other: "将近 %{count} 年前"
password_reset:
- no_token: "抱歉,该密码更改链接太旧。选择“登录”按钮并使用“我忘记密码了”获得新链接。"
choose_new: "选择一个新密码"
choose: "选择一个密码"
update: "更新密码"
@@ -3620,7 +3618,6 @@ zh_CN:
png_to_jpg_conversion_failure_message: "从 PNG 转换为 JPG 时出错。"
optimize_failure_message: "优化上传的图片时出错。"
download_failure: "从外部提供商下载文件失败。"
- size_mismatch_failure: "上传到 S3 的文件大小与外部上传存根的预期大小不匹配。%{additional_detail}"
create_multipart_failure: "无法在外部存储中创建分段上传。"
abort_multipart_failure: "无法中止外部存储中的分段上传。"
complete_multipart_failure: "无法在外部存储中完成分段上传。"
diff --git a/config/locales/server.zh_TW.yml b/config/locales/server.zh_TW.yml
index 26826072b3..e17a08f4ab 100644
--- a/config/locales/server.zh_TW.yml
+++ b/config/locales/server.zh_TW.yml
@@ -335,6 +335,7 @@ zh_TW:
你可以編輯你先前的回應訊息,選擇字句後點選 引用 來回應。
對於大家而言,閱讀包含較多回覆的少量貼文,會比閱讀多篇瑣碎貼文更為輕鬆。
+ dominating_topic: 您在這裡發表了超過 %{percent}% 的回覆;我們可以建議您給其他人一個加入討論的機會嗎?
too_many_replies: |
### 你的回應數量已經達到本主題的回覆上限
@@ -539,7 +540,6 @@ zh_TW:
almost_x_years:
other: "約 %{count} 年前"
password_reset:
- no_token: "抱歉,密碼修改連結已過期。選擇登錄按鈕再使用“我忘記了密碼”獲得一個新連結。"
choose_new: "選擇一個新密碼"
choose: "選擇一個密碼"
update: "更新密碼"
@@ -2586,6 +2586,7 @@ zh_TW:
file_missing: "抱歉,你必須選擇一個檔案上傳。"
empty: "很抱歉,您提供的文件是空的"
png_to_jpg_conversion_failure_message: "從PNG轉換為JPG時,發生了錯誤。"
+ size_mismatch_failure: "上傳到 S3 的檔案大小與外部上傳的預期大小不符。 %{additional_detail}"
attachments:
too_large: "抱歉,你試圖上傳的檔案太大了(最大限製為%{max_size_kb}%KB)。"
images:
@@ -2827,7 +2828,7 @@ zh_TW:
long_description: |
該徽章授與給分享連結給 1000 個其他訪客的你。哇!你把一個有意思的討論推廣給了廣大的讀者們,並且幫助社群前進了一大步!
first_like:
- name: 首次贊
+ name: 首次按讚
description: 已讚過了一個貼文
long_description: |
該徽章授予給第一次使用 :heart: 按鈕讚了貼文的成員。給貼文點贊是一個極好的讓社群成員知道他們的貼文有意思、有用、酷炫或者好玩的方法。分享愛!
diff --git a/config/nginx.sample.conf b/config/nginx.sample.conf
index 99f05f075f..fdb264d872 100644
--- a/config/nginx.sample.conf
+++ b/config/nginx.sample.conf
@@ -16,8 +16,12 @@ proxy_cache_path /var/nginx/cache inactive=1440m levels=1:2 keys_zone=one:10m ma
# Increased from the default value to acommodate large cookies during oAuth2 flows
# like in https://meta.discourse.org/t/x/74060 and large CSP and Link (preload) headers
-proxy_buffer_size 16k;
-proxy_buffers 4 16k;
+proxy_buffer_size 32k;
+proxy_buffers 4 32k;
+
+# Increased from the default value to allow for a large volume of cookies in request headers
+# Discourse itself tries to minimise cookie size, but we cannot control other cookies set by other tools on the same domain.
+large_client_header_buffers 4 32k;
# If you are going to use Puma, use these:
#
diff --git a/config/routes.rb b/config/routes.rb
index 27d7f27b6e..22e7389507 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -838,8 +838,8 @@ Discourse::Application.routes.draw do
get 'embed/count' => 'embed#count'
get 'embed/info' => 'embed#info'
- get "new-topic" => "list#latest"
- get "new-message" => "list#latest"
+ get "new-topic" => "new_topic#index"
+ get "new-message" => "new_topic#index"
# Topic routes
get "t/id_for/:slug" => "topics#id_for_slug"
diff --git a/config/site_settings.yml b/config/site_settings.yml
index 2145dbddd8..035c964f3e 100644
--- a/config/site_settings.yml
+++ b/config/site_settings.yml
@@ -379,7 +379,7 @@ basic:
enable_user_status:
client: true
default: false
- enable_onboarding_popups:
+ enable_user_tips:
client: true
default: false
@@ -2136,6 +2136,7 @@ backups:
include_s3_uploads_in_backups:
default: false
hidden: true
+ client: true
search:
use_pg_headlines_for_excerpt:
@@ -2532,6 +2533,8 @@ user_preferences:
default: 2
default_email_in_reply_to:
default: false
+ default_hide_profile_and_presence:
+ default: false
default_other_new_topic_duration_minutes:
enum: "NewTopicDurationSiteSetting"
diff --git a/db/migrate/20221101140632_rename_onboarding_popups_site_setting.rb b/db/migrate/20221101140632_rename_onboarding_popups_site_setting.rb
new file mode 100644
index 0000000000..da6fa2e5fc
--- /dev/null
+++ b/db/migrate/20221101140632_rename_onboarding_popups_site_setting.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class RenameOnboardingPopupsSiteSetting < ActiveRecord::Migration[7.0]
+ def up
+ execute "UPDATE site_settings SET name = 'enable_user_tips' WHERE name = 'enable_onboarding_popups'"
+ end
+
+ def down
+ execute "UPDATE site_settings SET name = 'enable_onboarding_popups' WHERE name = 'enable_user_tips'"
+ end
+end
diff --git a/db/migrate/20221101181505_hide_all_user_tips_for_existent_users.rb b/db/migrate/20221101181505_hide_all_user_tips_for_existent_users.rb
new file mode 100644
index 0000000000..4336add8e9
--- /dev/null
+++ b/db/migrate/20221101181505_hide_all_user_tips_for_existent_users.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class HideAllUserTipsForExistentUsers < ActiveRecord::Migration[7.0]
+ def up
+ execute "UPDATE user_options SET seen_popups = '{1, 2}'"
+ end
+
+ def down
+ execute "UPDATE user_options SET seen_popups = '{}'"
+ end
+end
diff --git a/db/migrate/20221103051248_remove_invalid_topic_allowed_users_from_invites.rb b/db/migrate/20221103051248_remove_invalid_topic_allowed_users_from_invites.rb
new file mode 100644
index 0000000000..d161a9f16d
--- /dev/null
+++ b/db/migrate/20221103051248_remove_invalid_topic_allowed_users_from_invites.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+class RemoveInvalidTopicAllowedUsersFromInvites < ActiveRecord::Migration[7.0]
+ def up
+ # We are getting all the topic_allowed_users records that
+ # match an invited user, which is created as part of the invite
+ # redemption flow. The original invite would _not_ have had a topic_invite
+ # record, and the user should have been added to the topic in the brief
+ # period between creation of the invited_users record and the update of
+ # that record.
+ #
+ # Having > 2 topic allowed users disqualifies messages sent only
+ # by the system or an admin to the user.
+ subquery_sql = <<~SQL
+ SELECT DISTINCT id
+ FROM (
+ SELECT tau.id, tau.user_id, COUNT(*) OVER (PARTITION BY tau.user_id)
+ FROM topic_allowed_users tau
+ JOIN invited_users iu ON iu.user_id = tau.user_id
+ LEFT JOIN topic_invites ti ON ti.invite_id = iu.invite_id AND tau.topic_id = ti.topic_id
+ WHERE ti.id IS NULL
+ AND tau.created_at BETWEEN iu.created_at AND iu.updated_at
+ AND iu.redeemed_at > '2022-10-27'
+ ) AS matching_topic_allowed_users
+ WHERE matching_topic_allowed_users.count > 2
+ SQL
+
+ # Back up the records we are going to change in case we are too
+ # brutal, and for further inspection.
+ #
+ # TODO DROP this table (topic_allowed_users_backup_nov_2022) in a later migration.
+ DB.exec(<<~SQL)
+ CREATE TABLE topic_allowed_users_backup_nov_2022
+ (
+ id INT NOT NULL,
+ user_id INT NOT NULL,
+ topic_id INT NOT NULL
+ );
+ INSERT INTO topic_allowed_users_backup_nov_2022(id, user_id, topic_id)
+ SELECT id, user_id, topic_id
+ FROM topic_allowed_users
+ WHERE id IN (
+ #{subquery_sql}
+ )
+ SQL
+
+ # Delete the invalid topic allowed users that should not be there.
+ DB.query(<<~SQL)
+ DELETE
+ FROM topic_allowed_users
+ WHERE id IN (
+ #{subquery_sql}
+ )
+ SQL
+ end
+
+ def down
+ raise ActiveRecord::IrreversibleMigration
+ end
+end
diff --git a/db/post_migrate/20221108032233_drop_old_bookmark_columns_v2.rb b/db/post_migrate/20221108032233_drop_old_bookmark_columns_v2.rb
new file mode 100644
index 0000000000..a0ad3fd3d1
--- /dev/null
+++ b/db/post_migrate/20221108032233_drop_old_bookmark_columns_v2.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require 'migration/column_dropper'
+
+class DropOldBookmarkColumnsV2 < ActiveRecord::Migration[7.0]
+ DROPPED_COLUMNS ||= {
+ bookmarks: %i{
+ post_id
+ for_topic
+ }
+ }
+
+ def up
+ DROPPED_COLUMNS.each do |table, columns|
+ Migration::ColumnDropper.execute_drop(table, columns)
+ end
+ end
+
+ def down
+ raise ActiveRecord::IrreversibleMigration
+ end
+end
diff --git a/lefthook.yml b/lefthook.yml
index c11d115f4a..be90ea676c 100644
--- a/lefthook.yml
+++ b/lefthook.yml
@@ -4,6 +4,9 @@ skip_output:
pre-commit:
parallel: true
+ skip:
+ - merge
+ - rebase
commands:
rubocop:
glob: "*.rb"
diff --git a/lib/guardian.rb b/lib/guardian.rb
index 0dbdd1dec3..83387fc7ba 100644
--- a/lib/guardian.rb
+++ b/lib/guardian.rb
@@ -359,8 +359,8 @@ class Guardian
flair_icon.present? || flair_upload_id.present?
end
- def can_change_primary_group?(user)
- user && is_staff?
+ def can_change_primary_group?(user, group)
+ user && can_edit_group?(group)
end
def can_change_trust_level?(user)
@@ -445,23 +445,42 @@ class Guardian
can_send_private_message?(group)
end
- def can_send_private_message?(target, notify_moderators: false)
- is_user = target.is_a?(User)
- is_group = target.is_a?(Group)
+ ##
+ # This should be used as a general, but not definitive, check for whether
+ # the user can send private messages _generally_, which is mostly useful
+ # for changing the UI.
+ #
+ # Please otherwise use can_send_private_message?(target, notify_moderators)
+ # to check if a single target can be messaged.
+ def can_send_private_messages?(notify_moderators: false)
from_system = @user.is_system_user?
from_bot = @user.bot?
- (is_group || is_user) &&
# User is authenticated
authenticated? &&
+ # User can send PMs, this can be covered by trust levels as well via AUTO_GROUPS
+ (is_staff? || from_bot || from_system || \
+ (@user.in_any_groups?(SiteSetting.personal_message_enabled_groups_map)) || notify_moderators)
+ end
+
+ ##
+ # This should be used as a final check for when a user is sending a message
+ # to a target user or group.
+ def can_send_private_message?(target, notify_moderators: false)
+ target_is_user = target.is_a?(User)
+ target_is_group = target.is_a?(Group)
+ from_system = @user.is_system_user?
+
+ # Must be a valid target
+ (target_is_group || target_is_user) &&
+ # User is authenticated and can send PMs, this can be covered by trust levels as well via AUTO_GROUPS
+ can_send_private_messages?(notify_moderators: notify_moderators) &&
# User disabled private message
- (is_staff? || is_group || target.user_option.allow_private_messages) &&
- # User can send PMs, this can be covered by trust levels as well via AUTO_GROUPS
- (is_staff? || from_bot || from_system || (@user.in_any_groups?(SiteSetting.personal_message_enabled_groups_map)) || notify_moderators) &&
+ (is_staff? || target_is_group || target.user_option.allow_private_messages) &&
# Can't send PMs to suspended users
- (is_staff? || is_group || !target.suspended?) &&
+ (is_staff? || target_is_group || !target.suspended?) &&
# Check group messageable level
- (from_system || is_user || Group.messageable(@user).where(id: target.id).exists? || notify_moderators) &&
+ (from_system || target_is_user || Group.messageable(@user).where(id: target.id).exists? || notify_moderators) &&
# Silenced users can only send PM to staff
(!is_silenced? || target.staff?)
end
diff --git a/lib/guardian/tag_guardian.rb b/lib/guardian/tag_guardian.rb
index 8d445fca9c..5a4be92ab4 100644
--- a/lib/guardian/tag_guardian.rb
+++ b/lib/guardian/tag_guardian.rb
@@ -15,8 +15,7 @@ module TagGuardian
return false if @user.blank?
return true if @user == Discourse.system_user
- # TODO (martin) Change to pm_tags_allowed_for_groups_map
- group_ids = SiteSetting.pm_tags_allowed_for_groups.to_s.split("|").map(&:to_i)
+ group_ids = SiteSetting.pm_tags_allowed_for_groups_map
group_ids.include?(Group::AUTO_GROUPS[:everyone]) || @user.group_users.exists?(group_id: group_ids)
end
diff --git a/lib/plugin/metadata.rb b/lib/plugin/metadata.rb
index 703c459f03..6eb4ffbd19 100644
--- a/lib/plugin/metadata.rb
+++ b/lib/plugin/metadata.rb
@@ -26,7 +26,6 @@ class Plugin::Metadata
"discourse-categories-suppressed",
"discourse-category-experts",
"discourse-characters-required",
- "discourse-chat",
"discourse-chat-integration",
"discourse-checklist",
"discourse-code-review",
@@ -60,11 +59,11 @@ class Plugin::Metadata
"discourse-linkedin-auth",
"discourse-microsoft-auth",
"discourse-policy",
+ "discourse-post-voting",
"discourse-presence",
"discourse-prometheus",
"discourse-prometheus-alert-receiver",
"discourse-push-notifications",
- "discourse-question-answer",
"discourse-reactions",
"discourse-restricted-replies",
"discourse-rss-polling",
@@ -82,15 +81,16 @@ class Plugin::Metadata
"discourse-teambuild",
"discourse-templates",
"discourse-tooltips",
+ "discourse-topic-voting",
"discourse-translator",
"discourse-user-card-badges",
"discourse-user-notes",
"discourse-vk-auth",
- "discourse-voting",
"discourse-whos-online",
"discourse-yearly-review",
"discourse-zendesk-plugin",
"docker_manager",
+ "chat",
"lazy-yt",
"poll",
"styleguide",
diff --git a/lib/s3_helper.rb b/lib/s3_helper.rb
index 859ca462ca..d1a08cef37 100644
--- a/lib/s3_helper.rb
+++ b/lib/s3_helper.rb
@@ -105,6 +105,15 @@ class S3Helper
rescue Aws::S3::Errors::NoSuchKey, Aws::S3::Errors::NotFound
end
+ def delete_objects(keys)
+ s3_bucket.delete_objects({
+ delete: {
+ objects: keys.map { |k| { key: k } },
+ quiet: true,
+ },
+ })
+ end
+
def copy(source, destination, options: {})
if options[:apply_metadata_to_destination]
options = options.except(:apply_metadata_to_destination).merge(metadata_directive: "REPLACE")
diff --git a/lib/search.rb b/lib/search.rb
index 2411574208..4e4df3d96d 100644
--- a/lib/search.rb
+++ b/lib/search.rb
@@ -446,10 +446,10 @@ class Search
def post_action_type_filter(posts, post_action_type)
posts.where("posts.id IN (
SELECT pa.post_id FROM post_actions pa
- WHERE pa.user_id = #{@guardian.user.id} AND
- pa.post_action_type_id = #{post_action_type} AND
+ WHERE pa.user_id = ? AND
+ pa.post_action_type_id = ? AND
deleted_at IS NULL
- )")
+ )", @guardian.user.id, post_action_type)
end
advanced_filter(/^in:(likes)$/i) do |posts, match|
@@ -464,17 +464,17 @@ class Search
# search based on a RegisteredBookmarkable's #search_query method.
advanced_filter(/^in:(bookmarks)$/i) do |posts, match|
if @guardian.user
- posts.where(<<~SQL)
+ posts.where(<<~SQL, @guardian.user.id)
posts.id IN (
SELECT bookmarkable_id FROM bookmarks
- WHERE bookmarks.user_id = #{@guardian.user.id} AND bookmarks.bookmarkable_type = 'Post'
+ WHERE bookmarks.user_id = ? AND bookmarks.bookmarkable_type = 'Post'
)
SQL
end
end
advanced_filter(/^in:posted$/i) do |posts|
- posts.where("posts.user_id = #{@guardian.user.id}") if @guardian.user
+ posts.where("posts.user_id = ?", @guardian.user.id) if @guardian.user
end
advanced_filter(/^in:(created|mine)$/i) do |posts|
@@ -640,7 +640,7 @@ class Search
advanced_filter(/^user:(.+)$/i) do |posts, match|
user_id = User.where(staged: false).where('username_lower = ? OR id = ?', match.downcase, match.to_i).pluck_first(:id)
if user_id
- posts.where("posts.user_id = #{user_id}")
+ posts.where("posts.user_id = ?", user_id)
else
posts.where("1 = 0")
end
@@ -656,7 +656,7 @@ class Search
end
if user_id
- posts.where("posts.user_id = #{user_id}")
+ posts.where("posts.user_id = ?", user_id)
else
posts.where("1 = 0")
end
@@ -1043,13 +1043,13 @@ class Search
posts.where("topics.category_id in (?)", category_ids)
elsif is_topic_search
- posts.where("topics.id = #{@search_context.id}")
+ posts.where("topics.id = ?", @search_context.id)
.order("posts.post_number #{@order == :latest ? "DESC" : ""}")
elsif @search_context.is_a?(Tag)
posts = posts
.joins("LEFT JOIN topic_tags ON topic_tags.topic_id = topics.id")
.joins("LEFT JOIN tags ON tags.id = topic_tags.tag_id")
- posts.where("tags.id = #{@search_context.id}")
+ posts.where("tags.id = ?", @search_context.id)
end
else
posts = categories_ignored(posts) unless @category_filter_matched
@@ -1243,8 +1243,8 @@ class Search
end
if min_id > 0
- low_set = query.dup.where("post_search_data.post_id < #{min_id}")
- high_set = query.where("post_search_data.post_id >= #{min_id}")
+ low_set = query.dup.where("post_search_data.post_id < ?", min_id)
+ high_set = query.where("post_search_data.post_id >= ?", min_id)
return { default: wrap_rows(high_set), remaining: wrap_rows(low_set) }
end
diff --git a/lib/shrink_uploaded_image.rb b/lib/shrink_uploaded_image.rb
index 3123468ef1..f6ef2354d4 100644
--- a/lib/shrink_uploaded_image.rb
+++ b/lib/shrink_uploaded_image.rb
@@ -83,7 +83,7 @@ class ShrinkUploadedImage
if post.raw_changed?
log "Updating post"
- elsif post.downloaded_images.has_value?(original_upload.id)
+ elsif post.post_hotlinked_media.exists?(upload_id: original_upload.id)
log "A hotlinked, unreferenced image"
elsif post.raw.include?(upload.short_url)
log "Already processed"
@@ -161,13 +161,10 @@ class ShrinkUploadedImage
)
end
- if existing_upload && post.downloaded_images.present?
- downloaded_images = post.downloaded_images.transform_values do |upload_id|
- upload_id == original_upload.id ? upload.id : upload_id
- end
-
- post.custom_fields[Post::DOWNLOADED_IMAGES] = downloaded_images
- post.save_custom_fields
+ if existing_upload
+ post.post_hotlinked_media
+ .where(upload_id: original_upload.id)
+ .update_all(upload_id: upload.id)
end
post.rebake!
diff --git a/lib/stylesheet/manager.rb b/lib/stylesheet/manager.rb
index 1ee4e8e557..97ac5d076c 100644
--- a/lib/stylesheet/manager.rb
+++ b/lib/stylesheet/manager.rb
@@ -6,10 +6,10 @@ require 'stylesheet/compiler'
module Stylesheet; end
class Stylesheet::Manager
+ BASE_COMPILER_VERSION = 1
CACHE_PATH ||= 'tmp/stylesheet-cache'
MANIFEST_DIR ||= "#{Rails.root}/tmp/cache/assets/#{Rails.env}"
- MANIFEST_FULL_PATH ||= "#{MANIFEST_DIR}/stylesheet-manifest"
THEME_REGEX ||= /_theme$/
COLOR_SCHEME_STYLESHEET ||= "color_definitions"
@@ -105,34 +105,65 @@ class Stylesheet::Manager
nil
end
- def self.last_file_updated
- if Rails.env.production?
- @last_file_updated ||= if File.exist?(MANIFEST_FULL_PATH)
- File.readlines(MANIFEST_FULL_PATH, 'r')[0]
+ def self.fs_asset_cachebuster
+ if use_file_hash_for_cachebuster?
+ @cachebuster ||= if File.exist?(manifest_full_path)
+ File.readlines(manifest_full_path, 'r')[0]
else
- mtime = max_file_mtime
+ cachebuster = "#{BASE_COMPILER_VERSION}:#{fs_assets_hash}"
FileUtils.mkdir_p(MANIFEST_DIR)
- File.open(MANIFEST_FULL_PATH, "w") { |f| f.print(mtime) }
- mtime
+ File.open(manifest_full_path, "w") { |f| f.print(cachebuster) }
+ cachebuster
end
else
- max_file_mtime
+ "#{BASE_COMPILER_VERSION}:#{max_file_mtime}"
end
end
- def self.max_file_mtime
- globs = ["#{Rails.root}/app/assets/stylesheets/**/*.*css",
- "#{Rails.root}/app/assets/images/**/*.*"]
+ def self.recalculate_fs_asset_cachebuster!
+ File.delete(manifest_full_path) if File.exist?(manifest_full_path)
+ @cachebuster = nil
+ fs_asset_cachebuster
+ end
- Discourse.plugins.map { |plugin| File.dirname(plugin.path) }.each do |path|
+ def self.manifest_full_path
+ path = "#{MANIFEST_DIR}/stylesheet-manifest"
+ return path if !Rails.env.test?
+ "#{path}-test_#{ENV['TEST_ENV_NUMBER'].presence || '0'}"
+ end
+ private_class_method :manifest_full_path
+
+ def self.use_file_hash_for_cachebuster?
+ Rails.env.production?
+ end
+ private_class_method :use_file_hash_for_cachebuster?
+
+ def self.list_files
+ globs = [
+ "#{Rails.root}/app/assets/stylesheets/**/*.*css",
+ "#{Rails.root}/app/assets/images/**/*.*"
+ ]
+
+ Discourse.plugins.each do |plugin|
+ path = File.dirname(plugin.path)
globs << "#{path}/plugin.rb"
globs << "#{path}/assets/stylesheets/**/*.*css"
end
- globs.map do |pattern|
- Dir.glob(pattern).map { |x| File.mtime(x) }.max
- end.compact.max.to_i
+ globs.flat_map { |g| Dir.glob(g) }.compact
end
+ private_class_method :list_files
+
+ def self.max_file_mtime
+ list_files.map { |x| File.mtime(x) }.compact.max.to_i
+ end
+ private_class_method :max_file_mtime
+
+ def self.fs_assets_hash
+ hashes = list_files.sort.map { |x| Digest::SHA1.hexdigest("#{x}: #{File.read(x)}") }
+ Digest::SHA1.hexdigest(hashes.join("|"))
+ end
+ private_class_method :fs_assets_hash
def self.cache_fullpath
path = "#{Rails.root}/#{CACHE_PATH}"
diff --git a/lib/stylesheet/manager/builder.rb b/lib/stylesheet/manager/builder.rb
index f4da9d14dc..25b1e078a8 100644
--- a/lib/stylesheet/manager/builder.rb
+++ b/lib/stylesheet/manager/builder.rb
@@ -13,7 +13,7 @@ class Stylesheet::Manager::Builder
def compile(opts = {})
if !opts[:force]
if File.exist?(stylesheet_fullpath)
- unless StylesheetCache.where(target: qualified_target, digest: digest).exists?
+ if !StylesheetCache.where(target: qualified_target, digest: digest).exists?
begin
source_map = begin
File.read(source_map_fullpath)
@@ -229,7 +229,7 @@ class Stylesheet::Manager::Builder
end
def default_digest
- Digest::SHA1.hexdigest "default-#{Stylesheet::Manager.last_file_updated}-#{plugins_digest}-#{current_hostname}"
+ Digest::SHA1.hexdigest "default-#{Stylesheet::Manager.fs_asset_cachebuster}-#{plugins_digest}-#{current_hostname}"
end
def color_scheme_digest
@@ -248,9 +248,9 @@ class Stylesheet::Manager::Builder
digest_string = "#{current_hostname}-"
if cs || categories_updated > 0
theme_color_defs = resolve_baked_field(:common, :color_definitions)
- digest_string += "#{RailsMultisite::ConnectionManagement.current_db}-#{cs&.id}-#{cs&.version}-#{theme_color_defs}-#{Stylesheet::Manager.last_file_updated}-#{categories_updated}-#{fonts}"
+ digest_string += "#{RailsMultisite::ConnectionManagement.current_db}-#{cs&.id}-#{cs&.version}-#{theme_color_defs}-#{Stylesheet::Manager.fs_asset_cachebuster}-#{categories_updated}-#{fonts}"
else
- digest_string += "defaults-#{Stylesheet::Manager.last_file_updated}-#{fonts}"
+ digest_string += "defaults-#{Stylesheet::Manager.fs_asset_cachebuster}-#{fonts}"
if cdn_url = GlobalSetting.cdn_url
digest_string += "-#{cdn_url}"
diff --git a/lib/tasks/assets.rake b/lib/tasks/assets.rake
index e69e192108..9082035725 100644
--- a/lib/tasks/assets.rake
+++ b/lib/tasks/assets.rake
@@ -64,6 +64,7 @@ task 'assets:precompile:css' => 'environment' do
STDERR.puts "-------------"
STDERR.puts "Compiling CSS for #{db} #{Time.zone.now}"
begin
+ Stylesheet::Manager.recalculate_fs_asset_cachebuster!
Stylesheet::Manager.precompile_css if db == "default"
Stylesheet::Manager.precompile_theme_css
rescue PG::UndefinedColumn, ActiveModel::MissingAttributeError, NoMethodError => e
diff --git a/lib/tasks/import.rake b/lib/tasks/import.rake
index 861b6d998a..7b1b02eeef 100644
--- a/lib/tasks/import.rake
+++ b/lib/tasks/import.rake
@@ -160,7 +160,8 @@ def insert_user_options
auto_track_topics_after_msecs,
notification_level_when_replying,
like_notification_frequency,
- skip_new_user_tips
+ skip_new_user_tips,
+ hide_profile_and_presence
)
SELECT u.id
, #{SiteSetting.default_email_mailing_list_mode}
@@ -181,6 +182,7 @@ def insert_user_options
, #{SiteSetting.default_other_notification_level_when_replying}
, #{SiteSetting.default_other_like_notification_frequency}
, #{SiteSetting.default_other_skip_new_user_tips}
+ , #{SiteSetting.default_hide_profile_and_presence}
FROM users u
LEFT JOIN user_options uo ON uo.user_id = u.id
WHERE uo.user_id IS NULL
diff --git a/lib/tasks/plugin.rake b/lib/tasks/plugin.rake
index e9b6218615..ddff652498 100644
--- a/lib/tasks/plugin.rake
+++ b/lib/tasks/plugin.rake
@@ -174,7 +174,9 @@ def spec(plugin, parallel: false)
params << "--seed #{ENV['RSPEC_SEED']}" if Integer(ENV['RSPEC_SEED'], exception: false)
ruby = `which ruby`.strip
- files = Dir.glob("./plugins/#{plugin}/spec/**/*_spec.rb").sort
+ # reject system specs as they are slow and need dedicated setup
+ files =
+ Dir.glob("./plugins/#{plugin}/spec/**/*_spec.rb").reject { |f| f.include?("spec/system/") }.sort
if files.length > 0
cmd = parallel ? "bin/turbo_rspec" : "bin/rspec"
sh "LOAD_PLUGINS=1 #{cmd} #{files.join(' ')} #{params.join(' ')}"
diff --git a/lib/tasks/posts.rake b/lib/tasks/posts.rake
index e3225f8ada..0304e80caa 100644
--- a/lib/tasks/posts.rake
+++ b/lib/tasks/posts.rake
@@ -506,7 +506,7 @@ def recover_uploads_from_index(path)
db = RailsMultisite::ConnectionManagement.current_db
cdn_path = SiteSetting.cdn_path("/uploads/#{db}").sub(/https?:/, "")
- Post.where("cooked LIKE '%#{cdn_path}%'").each do |post|
+ Post.where("cooked LIKE ?", "%#{cdn_path}%").each do |post|
regex = Regexp.new("((https?:)?#{Regexp.escape(cdn_path)}[^,;\\]\\>\\t\\n\\s)\"\']+)")
uploads = []
post.raw.scan(regex).each do |match|
@@ -663,9 +663,10 @@ def correct_inline_uploads
verbose = ENV["VERBOSE"]
scope = Post.joins(:upload_references).distinct("posts.id")
- .where(<<~SQL)
- raw LIKE '%/uploads/#{RailsMultisite::ConnectionManagement.current_db}/original/%'
- SQL
+ .where(
+ "raw LIKE ?",
+ "%/uploads/#{RailsMultisite::ConnectionManagement.current_db}/original/%",
+ )
affected_posts_count = scope.count
fixed_count = 0
diff --git a/lib/tasks/s3.rake b/lib/tasks/s3.rake
index b8aa43eee4..d2a21fed37 100644
--- a/lib/tasks/s3.rake
+++ b/lib/tasks/s3.rake
@@ -10,10 +10,18 @@ def gzip_s3_path(path)
"#{path[0..-ext.length]}gz#{ext}"
end
+def existing_assets
+ @existing_assets ||= Set.new(helper.list("assets/").map(&:key))
+end
+
+def prefix_s3_path(path)
+ path = File.join(helper.s3_bucket_folder_path, path) if helper.s3_bucket_folder_path
+ path
+end
+
def should_skip?(path)
return false if ENV['FORCE_S3_UPLOADS']
- @existing_assets ||= Set.new(helper.list("assets/").map(&:key))
- @existing_assets.include?(path)
+ existing_assets.include?(prefix_s3_path(path))
end
def upload(path, remote_path, content_type, content_encoding = nil)
@@ -196,26 +204,37 @@ end
task 's3:expire_missing_assets' => :environment do
ensure_s3_configured!
- count = 0
- keep = 0
+ puts "Checking for stale S3 assets..."
- in_manifest = asset_paths
+ assets_to_delete = existing_assets.dup
- puts "Ensuring AWS assets are tagged correctly for removal"
- helper.list('assets/').each do |f|
- if !in_manifest.include?(f.key)
- helper.tag_file(f.key, old: true)
- count += 1
- else
- # ensure we do not delete this by mistake
- helper.tag_file(f.key, {})
- keep += 1
+ # Check that all current assets are uploaded, and remove them from the to_delete list
+ asset_paths.each do |current_asset_path|
+ uploaded = assets_to_delete.delete?(prefix_s3_path(current_asset_path))
+ if !uploaded
+ puts "A current asset does not exist on S3 (#{current_asset_path}). Aborting cleanup task."
+ exit 1
end
end
- puts "#{count} assets were flagged for removal in 10 days (#{keep} assets will be retained)"
+ if assets_to_delete.size > 0
+ puts "Found #{assets_to_delete.size} assets to delete..."
- puts "Ensuring AWS rule exists for purging old assets"
- helper.update_lifecycle("delete_old_assets", 10, tag: { key: 'old', value: 'true' })
+ assets_to_delete.each do |to_delete|
+ if !to_delete.start_with?(prefix_s3_path("assets/"))
+ # Sanity check, this should never happen
+ raise "Attempted to delete a non-/asset S3 path (#{to_delete}). Aborting"
+ end
+ end
+ assets_to_delete.each_slice(500) do |slice|
+ message = "Deleting #{slice.size} assets...\n"
+ message += slice.join("\n").indent(2)
+ puts message
+ helper.delete_objects(slice)
+ puts "... done"
+ end
+ else
+ puts "No stale assets found"
+ end
end
diff --git a/lib/tasks/themes.rake b/lib/tasks/themes.rake
index b461e54b8b..289823643f 100644
--- a/lib/tasks/themes.rake
+++ b/lib/tasks/themes.rake
@@ -52,6 +52,12 @@ task "themes:install" => :environment do |task, args|
end
end
+desc "Install themes & theme components from an archive"
+task "themes:install:archive" => :environment do |task, args|
+ filename = ENV["THEME_ARCHIVE"]
+ RemoteTheme.update_zipped_theme(filename, File.basename(filename))
+end
+
def update_themes
Theme.includes(:remote_theme).where(enabled: true, auto_update: true).find_each do |theme|
begin
diff --git a/lib/tasks/uploads.rake b/lib/tasks/uploads.rake
index bfc2806faa..702f684625 100644
--- a/lib/tasks/uploads.rake
+++ b/lib/tasks/uploads.rake
@@ -31,7 +31,7 @@ def gather_uploads
puts "", "Gathering uploads for '#{current_db}'...", ""
Upload.where("url ~ '^\/uploads\/'")
- .where("url !~ '^\/uploads\/#{current_db}'")
+ .where("url !~ ?", "^\/uploads\/#{current_db}")
.find_each do |upload|
begin
old_db = upload.url[/^\/uploads\/([^\/]+)\//, 1]
@@ -1076,3 +1076,154 @@ task "uploads:fix_missing_s3" => :environment do
end
end
end
+
+# Supported ENV arguments:
+#
+# VERBOSE=1
+# Shows debug information.
+#
+# INTERACTIVE=1
+# Shows debug information and pauses for input on issues.
+#
+# WORKER_ID/WORKER_COUNT
+# When running the script on a single forum in multiple terminals.
+# For example, if you want 4 concurrent scripts use WORKER_COUNT=4
+# and WORKER_ID from 0 to 3.
+#
+# START_ID
+# Skip uploads with id lower than START_ID.
+task "uploads:downsize" => :environment do
+ min_image_pixels = 500_000 # 0.5 megapixels
+ default_image_pixels = 1_000_000 # 1 megapixel
+
+ max_image_pixels = [
+ ARGV[0]&.to_i || default_image_pixels,
+ min_image_pixels
+ ].max
+
+ ENV["VERBOSE"] = "1" if ENV["INTERACTIVE"]
+
+ def log(*args)
+ puts(*args) if ENV["VERBOSE"]
+ end
+
+ puts "", "Downsizing images to no more than #{max_image_pixels} pixels"
+
+ dimensions_count = 0
+ downsized_count = 0
+
+ scope = Upload
+ .by_users
+ .with_no_non_post_relations
+ .where("LOWER(extension) IN ('jpg', 'jpeg', 'gif', 'png')")
+
+ scope = scope.where(<<-SQL, max_image_pixels)
+ COALESCE(width, 0) = 0 OR
+ COALESCE(height, 0) = 0 OR
+ COALESCE(thumbnail_width, 0) = 0 OR
+ COALESCE(thumbnail_height, 0) = 0 OR
+ width * height > ?
+ SQL
+
+ if ENV["WORKER_ID"] && ENV["WORKER_COUNT"]
+ scope = scope.where("uploads.id % ? = ?", ENV["WORKER_COUNT"], ENV["WORKER_ID"])
+ end
+
+ if ENV["START_ID"]
+ scope = scope.where("uploads.id >= ?", ENV["START_ID"])
+ end
+
+ skipped = 0
+ total_count = scope.count
+ puts "Uploads to process: #{total_count}"
+
+ scope.find_each.with_index do |upload, index|
+ progress = (index * 100.0 / total_count).round(1)
+
+ log "\n"
+ print "\r#{progress}% Fixed dimensions: #{dimensions_count} Downsized: #{downsized_count} Skipped: #{skipped} (upload id: #{upload.id})"
+ log "\n"
+
+ path = if upload.local?
+ Discourse.store.path_for(upload)
+ else
+ (Discourse.store.download(upload, max_file_size_kb: 100.megabytes) rescue nil)&.path
+ end
+
+ unless path
+ log "No image path"
+ skipped += 1
+ next
+ end
+
+ begin
+ w, h = FastImage.size(path, raise_on_failure: true)
+ rescue FastImage::UnknownImageType
+ log "Unknown image type"
+ skipped += 1
+ next
+ rescue FastImage::SizeNotFound
+ log "Size not found"
+ skipped += 1
+ next
+ end
+
+ if !w || !h
+ log "Invalid image dimensions"
+ skipped += 1
+ next
+ end
+
+ ww, hh = ImageSizer.resize(w, h)
+
+ if w == 0 || h == 0 || ww == 0 || hh == 0
+ log "Invalid image dimensions"
+ skipped += 1
+ next
+ end
+
+ upload.attributes = {
+ width: w,
+ height: h,
+ thumbnail_width: ww,
+ thumbnail_height: hh,
+ filesize: File.size(path)
+ }
+
+ if upload.changed?
+ log "Correcting the upload dimensions"
+ log "Before: #{upload.width_was}x#{upload.height_was} #{upload.thumbnail_width_was}x#{upload.thumbnail_height_was} (#{upload.filesize_was})"
+ log "After: #{w}x#{h} #{ww}x#{hh} (#{upload.filesize})"
+
+ dimensions_count += 1
+
+ # Don't validate the size - max image size setting might have
+ # changed since the file was uploaded, so this could fail
+ upload.validate_file_size = false
+ upload.save!
+ end
+
+ if w * h < max_image_pixels
+ log "Image size within allowed range"
+ skipped += 1
+ next
+ end
+
+ result = ShrinkUploadedImage.new(
+ upload: upload,
+ path: path,
+ max_pixels: max_image_pixels,
+ verbose: ENV["VERBOSE"],
+ interactive: ENV["INTERACTIVE"]
+ ).perform
+
+ if result
+ downsized_count += 1
+ else
+ skipped += 1
+ end
+ end
+
+ STDIN.beep
+ puts "", "Done", Time.zone.now
+end
diff --git a/lib/theme_store/git_importer.rb b/lib/theme_store/git_importer.rb
index c386fea5ee..93878b4e21 100644
--- a/lib/theme_store/git_importer.rb
+++ b/lib/theme_store/git_importer.rb
@@ -117,36 +117,44 @@ class ThemeStore::GitImporter
end
def clone_http!
+ uris = [@uri]
+
begin
- @uri = FinalDestination.resolve(@uri.to_s)
+ resolved_uri = FinalDestination.resolve(@uri.to_s)
+ if resolved_uri && resolved_uri != @uri
+ uris.unshift(resolved_uri)
+ end
rescue
- raise_import_error!
+ # If this fails, we can stil attempt to clone using the original URI
end
- @url = @uri.to_s
+ uris.each do |uri|
+ @uri = uri
+ @url = @uri.to_s
- unless ["http", "https"].include?(@uri.scheme)
- raise_import_error!
+ unless ["http", "https"].include?(@uri.scheme)
+ raise_import_error!
+ end
+
+ addresses = FinalDestination::SSRFDetector.lookup_and_filter_ips(@uri.host)
+
+ unless addresses.empty?
+ env = { "GIT_TERMINAL_PROMPT" => "0" }
+
+ args = clone_args(
+ "http.followRedirects" => "false",
+ "http.curloptResolve" => "#{@uri.host}:#{@uri.port}:#{addresses.join(',')}",
+ )
+
+ begin
+ Discourse::Utils.execute_command(env, *args, timeout: COMMAND_TIMEOUT_SECONDS)
+ return
+ rescue RuntimeError
+ end
+ end
end
- addresses = FinalDestination::SSRFDetector.lookup_and_filter_ips(@uri.host)
-
- if addresses.empty?
- raise_import_error!
- end
-
- env = { "GIT_TERMINAL_PROMPT" => "0" }
-
- args = clone_args(
- "http.followRedirects" => "false",
- "http.curloptResolve" => "#{@uri.host}:#{@uri.port}:#{addresses.join(',')}",
- )
-
- begin
- Discourse::Utils.execute_command(env, *args, timeout: COMMAND_TIMEOUT_SECONDS)
- rescue RuntimeError
- raise_import_error!
- end
+ raise_import_error!
end
def clone_ssh!
diff --git a/lib/topic_query.rb b/lib/topic_query.rb
index ec75d18206..c5b5b84b94 100644
--- a/lib/topic_query.rb
+++ b/lib/topic_query.rb
@@ -311,9 +311,9 @@ class TopicQuery
def list_group_topics(group)
list = default_results.where("
topics.user_id IN (
- SELECT user_id FROM group_users gu WHERE gu.group_id = #{group.id.to_i}
+ SELECT user_id FROM group_users gu WHERE gu.group_id = ?
)
- ")
+ ", group.id.to_i)
create_list(:group_topics, {}, list)
end
diff --git a/lib/topic_query/private_message_lists.rb b/lib/topic_query/private_message_lists.rb
index 395b54fad5..c8883169a9 100644
--- a/lib/topic_query/private_message_lists.rb
+++ b/lib/topic_query/private_message_lists.rb
@@ -118,20 +118,20 @@ class TopicQuery
result = result.joins("INNER JOIN group_users gu ON gu.group_id = tag.group_id AND gu.user_id = #{user.id.to_i}")
end
elsif type == :user
- result = result.where("topics.id IN (SELECT topic_id FROM topic_allowed_users WHERE user_id = #{user.id.to_i})")
+ result = result.where("topics.id IN (SELECT topic_id FROM topic_allowed_users WHERE user_id = ?)", user.id.to_i)
elsif type == :all
group_ids = group_with_messages_ids(user)
result =
if group_ids.present?
- result.where(<<~SQL)
+ result.where(<<~SQL, user.id.to_i, group_ids)
topics.id IN (
SELECT topic_id
FROM topic_allowed_users
- WHERE user_id = #{user.id.to_i}
+ WHERE user_id = ?
UNION ALL
SELECT topic_id FROM topic_allowed_groups
- WHERE group_id IN (#{group_ids.join(",")})
+ WHERE group_id IN (?)
)
SQL
else
@@ -259,10 +259,10 @@ class TopicQuery
end
def have_posts_from_others(list, user)
- list.where(<<~SQL)
+ list.where(<<~SQL, user.id.to_i)
NOT (
topics.participant_count = 1
- AND topics.user_id = #{user.id.to_i}
+ AND topics.user_id = ?
AND topics.moderator_posts_count = 0
)
SQL
diff --git a/lib/topic_query_params.rb b/lib/topic_query_params.rb
index 7081513c3f..5c0175a8ba 100644
--- a/lib/topic_query_params.rb
+++ b/lib/topic_query_params.rb
@@ -30,7 +30,6 @@ module TopicQueryParams
def hide_welcome_topic?
return false if !SiteSetting.bootstrap_mode_enabled
- return false if @guardian.is_admin?
Site.welcome_topic_exists_and_is_not_edited?
end
end
diff --git a/lib/validators/upload_validator.rb b/lib/validators/upload_validator.rb
index 7dd4f75781..5fb084c835 100644
--- a/lib/validators/upload_validator.rb
+++ b/lib/validators/upload_validator.rb
@@ -2,10 +2,7 @@
require "file_helper"
-module Validators; end
-
class UploadValidator < ActiveModel::Validator
-
def validate(upload)
# staff can upload any file in PM
if (upload.for_private_message && SiteSetting.allow_staff_to_upload_any_file_in_pm)
@@ -141,6 +138,8 @@ class UploadValidator < ActiveModel::Validator
end
def maximum_file_size(upload, type)
+ return if !upload.validate_file_size
+
max_size_kb = if upload.for_export
SiteSetting.max_export_file_size_kb
else
@@ -157,5 +156,4 @@ class UploadValidator < ActiveModel::Validator
upload.errors.add(:filesize, message)
end
end
-
end
diff --git a/lib/version.rb b/lib/version.rb
index d3777983db..922ef2e01c 100644
--- a/lib/version.rb
+++ b/lib/version.rb
@@ -10,7 +10,7 @@ module Discourse
MAJOR = 2
MINOR = 9
TINY = 0
- PRE = 'beta11'
+ PRE = 'beta12'
STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
end
diff --git a/package.json b/package.json
index 5db51f607f..67554baeb0 100644
--- a/package.json
+++ b/package.json
@@ -11,7 +11,6 @@
"@highlightjs/cdn-assets": "^11.6.0",
"@json-editor/json-editor": "^2.6.1",
"ace-builds": "1.4.13",
- "bootbox": "3.2.0",
"chart.js": "3.5.1",
"chartjs-plugin-datalabels": "^2.0.0",
"diffhtml": "^1.0.0-beta.20",
@@ -29,10 +28,10 @@
"workbox-sw": "^4.3.1"
},
"devDependencies": {
- "@arkweid/lefthook": "^0.7.7",
"@mixer/parallel-prettier": "^2.0.1",
"chrome-launcher": "^0.15.0",
"chrome-remote-interface": "^0.31.2",
+ "lefthook": "^1.2.0",
"puppeteer-core": "^13.7.0"
},
"scripts": {
diff --git a/plugins/chat/README.md b/plugins/chat/README.md
new file mode 100644
index 0000000000..fc0b204240
--- /dev/null
+++ b/plugins/chat/README.md
@@ -0,0 +1,54 @@
+:warning: This plugin is still in active development and may change frequently
+
+## Documentation
+
+The Discourse Chat plugin adds chat functionality to your Discourse so it can natively support both long-form and short-form communication needs of your online community.
+
+For documentation, see [Discourse Chat](https://meta.discourse.org/t/discourse-chat/230881)
+
+## Plugin API
+
+### registerChatComposerButton
+
+#### Usage
+
+```javascript
+api.registerChatComposerButton({ id: "foo", ... });
+```
+
+#### Options
+
+Every option accepts a `value` or a `function`, when passing a function `this` will be the `chat-composer` component instance. Example of an option using a function:
+
+```javascript
+api.registerChatComposerButton({
+ id: "foo",
+ displayed() {
+ return this.site.mobileView && this.canAttachUploads;
+ },
+});
+```
+
+##### Required
+
+- `id` unique, used to identify your button, eg: "gifs"
+- `action` callback when the button is pressed, can be an action name or an anonymous function, eg: "onFooClicked" or `() => { console.log("clicked") }`
+
+A button requires at least an icon or a label:
+
+- `icon`, eg: "times"
+- `label`, text displayed on the button, a translatable key, eg: "foo.bar"
+- `translatedLabel`, text displayed on the button, a string, eg: "Add gifs"
+
+##### Optional
+
+- `position`, can be "inline" or "dropdown", defaults to "inline"
+- `title`, title attribute of the button, a translatable key, eg: "foo.bar"
+- `translatedTitle`, title attribute of the button, a string, eg: "Add gifs"
+- `ariaLabel`, aria-label attribute of the button, a translatable key, eg: "foo.bar"
+- `translatedAriaLabel`, aria-label attribute of the button, a string, eg: "Add gifs"
+- `classNames`, additional names to add to the button’s class attribute, eg: ["foo", "bar"]
+- `displayed`, hide/or show the button, expects a boolean
+- `disabled`, sets the disabled attribute on the button, expects a boolean
+- `priority`, an integer defining the order of the buttons, higher comes first, eg: `700`
+- `dependentKeys`, list of property names which should trigger a refresh of the buttons when changed, eg: `["foo.bar", "bar.baz"]`
diff --git a/plugins/chat/app/controllers/admin/admin_incoming_chat_webhooks_controller.rb b/plugins/chat/app/controllers/admin/admin_incoming_chat_webhooks_controller.rb
new file mode 100644
index 0000000000..24bcd25abd
--- /dev/null
+++ b/plugins/chat/app/controllers/admin/admin_incoming_chat_webhooks_controller.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+class Chat::AdminIncomingChatWebhooksController < Admin::AdminController
+ requires_plugin Chat::PLUGIN_NAME
+
+ def index
+ render_serialized(
+ {
+ chat_channels: ChatChannel.public_channels,
+ incoming_chat_webhooks: IncomingChatWebhook.includes(:chat_channel).all,
+ },
+ AdminChatIndexSerializer,
+ root: false,
+ )
+ end
+
+ def create
+ params.require(%i[name chat_channel_id])
+
+ chat_channel = ChatChannel.find_by(id: params[:chat_channel_id])
+ raise Discourse::NotFound if chat_channel.nil? || chat_channel.direct_message_channel?
+
+ webhook = IncomingChatWebhook.new(name: params[:name], chat_channel: chat_channel)
+ if webhook.save
+ render_serialized(webhook, IncomingChatWebhookSerializer, root: false)
+ else
+ render_json_error(webhook)
+ end
+ end
+
+ def update
+ params.require(%i[incoming_chat_webhook_id name chat_channel_id])
+
+ webhook = IncomingChatWebhook.find_by(id: params[:incoming_chat_webhook_id])
+ raise Discourse::NotFound unless webhook
+
+ chat_channel = ChatChannel.find_by(id: params[:chat_channel_id])
+ raise Discourse::NotFound if chat_channel.nil? || chat_channel.direct_message_channel?
+
+ if webhook.update(
+ name: params[:name],
+ description: params[:description],
+ emoji: params[:emoji],
+ username: params[:username],
+ chat_channel: chat_channel,
+ )
+ render json: success_json
+ else
+ render_json_error(webhook)
+ end
+ end
+
+ def destroy
+ params.require(:incoming_chat_webhook_id)
+
+ webhook = IncomingChatWebhook.find_by(id: params[:incoming_chat_webhook_id])
+ webhook.destroy if webhook
+ render json: success_json
+ end
+end
diff --git a/plugins/chat/app/controllers/api/category_chatables_controller.rb b/plugins/chat/app/controllers/api/category_chatables_controller.rb
new file mode 100644
index 0000000000..50fe11edc7
--- /dev/null
+++ b/plugins/chat/app/controllers/api/category_chatables_controller.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+class Chat::Api::CategoryChatablesController < ApplicationController
+ def permissions
+ category = Category.find(params[:id])
+
+ if category.read_restricted?
+ permissions =
+ Group
+ .joins(:category_groups)
+ .where(category_groups: { category_id: category.id })
+ .joins("LEFT OUTER JOIN group_users ON groups.id = group_users.group_id")
+ .group("groups.id", "groups.name")
+ .pluck("groups.name", "COUNT(group_users.user_id)")
+
+ group_names = permissions.map { |p| "@#{p[0]}" }
+ members_count = permissions.sum { |p| p[1].to_i }
+
+ permissions_result = {
+ allowed_groups: group_names,
+ members_count: members_count,
+ private: true,
+ }
+ else
+ everyone_group = Group.find(Group::AUTO_GROUPS[:everyone])
+
+ permissions_result = { allowed_groups: ["@#{everyone_group.name}"], private: false }
+ end
+
+ render json: permissions_result
+ end
+end
diff --git a/plugins/chat/app/controllers/api/chat_channel_memberships_controller.rb b/plugins/chat/app/controllers/api/chat_channel_memberships_controller.rb
new file mode 100644
index 0000000000..727811c9ca
--- /dev/null
+++ b/plugins/chat/app/controllers/api/chat_channel_memberships_controller.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class Chat::Api::ChatChannelMembershipsController < Chat::Api::ChatChannelsController
+ def index
+ channel = find_chat_channel
+
+ offset = (params[:offset] || 0).to_i
+ limit = (params[:limit] || 50).to_i.clamp(1, 50)
+
+ memberships =
+ ChatChannelMembershipsQuery.call(
+ channel,
+ offset: offset,
+ limit: limit,
+ username: params[:username],
+ )
+
+ render_serialized(memberships, UserChatChannelMembershipSerializer, root: false)
+ end
+end
diff --git a/plugins/chat/app/controllers/api/chat_channel_notifications_settings_controller.rb b/plugins/chat/app/controllers/api/chat_channel_notifications_settings_controller.rb
new file mode 100644
index 0000000000..57c0055424
--- /dev/null
+++ b/plugins/chat/app/controllers/api/chat_channel_notifications_settings_controller.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+MEMBERSHIP_EDITABLE_PARAMS = %i[muted desktop_notification_level mobile_notification_level]
+
+class Chat::Api::ChatChannelNotificationsSettingsController < Chat::Api::ChatChannelsController
+ def update
+ settings_params = params.permit(MEMBERSHIP_EDITABLE_PARAMS)
+ membership = find_membership
+ membership.update!(settings_params.to_h)
+ render_serialized(membership, UserChatChannelMembershipSerializer, root: false)
+ end
+end
diff --git a/plugins/chat/app/controllers/api/chat_channels_controller.rb b/plugins/chat/app/controllers/api/chat_channels_controller.rb
new file mode 100644
index 0000000000..b073936cf4
--- /dev/null
+++ b/plugins/chat/app/controllers/api/chat_channels_controller.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+CHAT_CHANNEL_EDITABLE_PARAMS = %i[name description]
+CATEGORY_CHAT_CHANNEL_EDITABLE_PARAMS = %i[auto_join_users]
+
+class Chat::Api::ChatChannelsController < Chat::Api
+ def index
+ options = { status: params[:status] ? ChatChannel.statuses[params[:status]] : nil }.merge(
+ params.permit(:filter, :limit, :offset),
+ ).symbolize_keys!
+
+ memberships = Chat::ChatChannelMembershipManager.all_for_user(current_user)
+ channels = Chat::ChatChannelFetcher.secured_public_channels(guardian, memberships, options)
+
+ serialized_channels =
+ channels.map do |channel|
+ ChatChannelSerializer.new(
+ channel,
+ scope: Guardian.new(current_user),
+ membership: memberships.find { |membership| membership.chat_channel_id == channel.id },
+ )
+ end
+ render json: serialized_channels, root: false
+ end
+
+ def update
+ guardian.ensure_can_edit_chat_channel!
+
+ chat_channel = find_chat_channel
+
+ if chat_channel.direct_message_channel?
+ raise Discourse::InvalidParameters.new(
+ I18n.t("chat.errors.cant_update_direct_message_channel"),
+ )
+ end
+
+ params_to_edit = editable_params(params, chat_channel)
+ params_to_edit.each { |k, v| params_to_edit[k] = nil if params_to_edit[k].blank? }
+
+ if ActiveRecord::Type::Boolean.new.deserialize(params_to_edit[:auto_join_users])
+ auto_join_limiter(chat_channel).performed!
+ end
+
+ chat_channel.update!(params_to_edit)
+
+ ChatPublisher.publish_chat_channel_edit(chat_channel, current_user)
+
+ if chat_channel.category_channel? && chat_channel.auto_join_users
+ Chat::ChatChannelMembershipManager.new(chat_channel).enforce_automatic_channel_memberships
+ end
+
+ render_serialized(
+ chat_channel,
+ ChatChannelSerializer,
+ root: false,
+ membership: chat_channel.membership_for(current_user),
+ )
+ end
+
+ private
+
+ def find_chat_channel
+ chat_channel = ChatChannel.find(params.require(:chat_channel_id))
+ guardian.ensure_can_see_chat_channel!(chat_channel)
+ chat_channel
+ end
+
+ def find_membership
+ chat_channel = find_chat_channel
+ membership = Chat::ChatChannelMembershipManager.new(chat_channel).find_for_user(current_user)
+ raise Discourse::NotFound if membership.blank?
+ membership
+ end
+
+ def auto_join_limiter(chat_channel)
+ RateLimiter.new(
+ current_user,
+ "auto_join_users_channel_#{chat_channel.id}",
+ 1,
+ 3.minutes,
+ apply_limit_to_staff: true,
+ )
+ end
+
+ def editable_params(params, chat_channel)
+ permitted_params = CHAT_CHANNEL_EDITABLE_PARAMS
+
+ permitted_params += CATEGORY_CHAT_CHANNEL_EDITABLE_PARAMS if chat_channel.category_channel?
+
+ params.permit(*permitted_params)
+ end
+end
diff --git a/plugins/chat/app/controllers/api_controller.rb b/plugins/chat/app/controllers/api_controller.rb
new file mode 100644
index 0000000000..eaf3db9be5
--- /dev/null
+++ b/plugins/chat/app/controllers/api_controller.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class Chat::Api < Chat::ChatBaseController
+ before_action :ensure_logged_in
+ before_action :ensure_can_chat
+
+ private
+
+ def ensure_can_chat
+ raise Discourse::NotFound unless SiteSetting.chat_enabled
+ guardian.ensure_can_chat!(current_user)
+ end
+end
diff --git a/plugins/chat/app/controllers/chat_base_controller.rb b/plugins/chat/app/controllers/chat_base_controller.rb
new file mode 100644
index 0000000000..14cc69f271
--- /dev/null
+++ b/plugins/chat/app/controllers/chat_base_controller.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class Chat::ChatBaseController < ::ApplicationController
+ before_action :ensure_logged_in
+ before_action :ensure_can_chat
+
+ private
+
+ def ensure_can_chat
+ raise Discourse::NotFound unless SiteSetting.chat_enabled
+ guardian.ensure_can_chat!(current_user)
+ end
+
+ def set_channel_and_chatable_with_access_check(chat_channel_id: nil)
+ params.require(:chat_channel_id) if chat_channel_id.blank?
+ id_or_name = chat_channel_id || params[:chat_channel_id]
+ @chat_channel = Chat::ChatChannelFetcher.find_with_access_check(id_or_name, guardian)
+ @chatable = @chat_channel.chatable
+ end
+end
diff --git a/plugins/chat/app/controllers/chat_channels_controller.rb b/plugins/chat/app/controllers/chat_channels_controller.rb
new file mode 100644
index 0000000000..cecc5b2f1f
--- /dev/null
+++ b/plugins/chat/app/controllers/chat_channels_controller.rb
@@ -0,0 +1,250 @@
+# frozen_string_literal: true
+
+class Chat::ChatChannelsController < Chat::ChatBaseController
+ before_action :set_channel_and_chatable_with_access_check, except: %i[index create search]
+
+ def index
+ structured = Chat::ChatChannelFetcher.structured(guardian)
+ render_serialized(structured, ChatChannelIndexSerializer, root: false)
+ end
+
+ def show
+ render_serialized(
+ @chat_channel,
+ ChatChannelSerializer,
+ membership: @chat_channel.membership_for(current_user),
+ root: false,
+ )
+ end
+
+ def follow
+ membership = @chat_channel.add(current_user)
+
+ render_serialized(@chat_channel, ChatChannelSerializer, membership: membership, root: false)
+ end
+
+ def unfollow
+ membership = @chat_channel.remove(current_user)
+
+ render_serialized(@chat_channel, ChatChannelSerializer, membership: membership, root: false)
+ end
+
+ def create
+ params.require(%i[id name])
+ guardian.ensure_can_create_chat_channel!
+ if params[:name].length > SiteSetting.max_topic_title_length
+ raise Discourse::InvalidParameters.new(:name)
+ end
+
+ exists =
+ ChatChannel.exists?(chatable_type: "Category", chatable_id: params[:id], name: params[:name])
+ if exists
+ raise Discourse::InvalidParameters.new(I18n.t("chat.errors.channel_exists_for_category"))
+ end
+
+ chatable = Category.find_by(id: params[:id])
+ raise Discourse::NotFound unless chatable
+
+ auto_join_users = ActiveRecord::Type::Boolean.new.deserialize(params[:auto_join_users]) || false
+
+ chat_channel =
+ chatable.create_chat_channel!(
+ name: params[:name],
+ description: params[:description],
+ user_count: 1,
+ auto_join_users: auto_join_users,
+ )
+ chat_channel.user_chat_channel_memberships.create!(user: current_user, following: true)
+
+ if chat_channel.auto_join_users
+ Chat::ChatChannelMembershipManager.new(chat_channel).enforce_automatic_channel_memberships
+ end
+
+ render_serialized(
+ chat_channel,
+ ChatChannelSerializer,
+ membership: chat_channel.membership_for(current_user),
+ )
+ end
+
+ def edit
+ guardian.ensure_can_edit_chat_channel!
+ if (params[:name]&.length || 0) > SiteSetting.max_topic_title_length
+ raise Discourse::InvalidParameters.new(:name)
+ end
+
+ chat_channel = ChatChannel.find_by(id: params[:chat_channel_id])
+ raise Discourse::NotFound unless chat_channel
+
+ chat_channel.name = params[:name] if params[:name]
+ chat_channel.description = params[:description] if params[:description]
+ chat_channel.save!
+
+ ChatPublisher.publish_chat_channel_edit(chat_channel, current_user)
+ render_serialized(
+ chat_channel,
+ ChatChannelSerializer,
+ membership: chat_channel.membership_for(current_user),
+ )
+ end
+
+ def search
+ params.require(:filter)
+ filter = params[:filter]&.downcase
+ memberships = Chat::ChatChannelMembershipManager.all_for_user(current_user)
+ public_channels =
+ Chat::ChatChannelFetcher.secured_public_channels(
+ guardian,
+ memberships,
+ filter: filter,
+ status: :open,
+ )
+
+ users = User.joins(:user_option).where.not(id: current_user.id)
+ if !Chat.allowed_group_ids.include?(Group::AUTO_GROUPS[:everyone])
+ users =
+ users
+ .joins(:groups)
+ .where(groups: { id: Chat.allowed_group_ids })
+ .or(users.joins(:groups).staff)
+ end
+
+ users = users.where(user_option: { chat_enabled: true })
+ like_filter = "%#{filter}%"
+ if SiteSetting.prioritize_username_in_ux || !SiteSetting.enable_names
+ users = users.where("users.username_lower ILIKE ?", like_filter)
+ else
+ users =
+ users.where(
+ "LOWER(users.name) ILIKE ? OR users.username_lower ILIKE ?",
+ like_filter,
+ like_filter,
+ )
+ end
+
+ users = users.limit(25).uniq
+
+ direct_message_channels =
+ (
+ if users.count > 0
+ ChatChannel
+ .includes(chatable: :users)
+ .joins(direct_message: :direct_message_users)
+ .group(1)
+ .having(
+ "ARRAY[?] <@ ARRAY_AGG(user_id) AND ARRAY[?] && ARRAY_AGG(user_id)",
+ [current_user.id],
+ users.map(&:id),
+ )
+ else
+ []
+ end
+ )
+
+ user_ids_with_channel = []
+ direct_message_channels.each do |dm_channel|
+ user_ids = dm_channel.chatable.users.map(&:id)
+ user_ids_with_channel.concat(user_ids) if user_ids.count < 3
+ end
+
+ users_without_channel = users.filter { |u| !user_ids_with_channel.include?(u.id) }
+
+ if current_user.username.downcase.start_with?(filter)
+ # We filtered out the current user for the query earlier, but check to see
+ # if they should be included, and add.
+ users_without_channel << current_user
+ end
+
+ render_serialized(
+ {
+ public_channels: public_channels,
+ direct_message_channels: direct_message_channels,
+ users: users_without_channel,
+ memberships: memberships,
+ },
+ ChatChannelSearchSerializer,
+ root: false,
+ )
+ end
+
+ def archive
+ params.require(:type)
+
+ if params[:type] == "newTopic" ? params[:title].blank? : params[:topic_id].blank?
+ raise Discourse::InvalidParameters
+ end
+
+ if !guardian.can_change_channel_status?(@chat_channel, :read_only)
+ raise Discourse::InvalidAccess.new(I18n.t("chat.errors.channel_cannot_be_archived"))
+ end
+
+ Chat::ChatChannelArchiveService.begin_archive_process(
+ chat_channel: @chat_channel,
+ acting_user: current_user,
+ topic_params: {
+ topic_id: params[:topic_id],
+ topic_title: params[:title],
+ category_id: params[:category_id],
+ tags: params[:tags],
+ },
+ )
+
+ render json: success_json
+ end
+
+ def retry_archive
+ guardian.ensure_can_change_channel_status!(@chat_channel, :archived)
+
+ archive = @chat_channel.chat_channel_archive
+ raise Discourse::NotFound if archive.blank?
+ raise Discourse::InvalidAccess if !archive.failed?
+
+ Chat::ChatChannelArchiveService.retry_archive_process(chat_channel: @chat_channel)
+
+ render json: success_json
+ end
+
+ def change_status
+ params.require(:status)
+
+ # we only want to use this endpoint for open/closed status changes,
+ # the others are more "special" and are handled by the archive endpoint
+ if !ChatChannel.statuses.keys.include?(params[:status]) || params[:status] == "read_only" ||
+ params[:status] == "archive"
+ raise Discourse::InvalidParameters
+ end
+
+ guardian.ensure_can_change_channel_status!(@chat_channel, params[:status].to_sym)
+ @chat_channel.public_send("#{params[:status]}!", current_user)
+
+ render json: success_json
+ end
+
+ def destroy
+ params.require(:channel_name_confirmation)
+
+ guardian.ensure_can_delete_chat_channel!
+
+ if @chat_channel.title(current_user).downcase != params[:channel_name_confirmation].downcase
+ raise Discourse::InvalidParameters.new(:channel_name_confirmation)
+ end
+
+ begin
+ ChatChannel.transaction do
+ @chat_channel.trash!(current_user)
+ StaffActionLogger.new(current_user).log_custom(
+ "chat_channel_delete",
+ {
+ chat_channel_id: @chat_channel.id,
+ chat_channel_name: @chat_channel.title(current_user),
+ },
+ )
+ end
+ rescue ActiveRecord::Rollback
+ return render_json_error(I18n.t("chat.errors.delete_channel_failed"))
+ end
+
+ Jobs.enqueue(:chat_channel_delete, { chat_channel_id: @chat_channel.id })
+ render json: success_json
+ end
+end
diff --git a/plugins/chat/app/controllers/chat_controller.rb b/plugins/chat/app/controllers/chat_controller.rb
new file mode 100644
index 0000000000..3f78f15b8e
--- /dev/null
+++ b/plugins/chat/app/controllers/chat_controller.rb
@@ -0,0 +1,501 @@
+# frozen_string_literal: true
+
+class Chat::ChatController < Chat::ChatBaseController
+ PAST_MESSAGE_LIMIT = 20
+ FUTURE_MESSAGE_LIMIT = 40
+ PAST = "past"
+ FUTURE = "future"
+ CHAT_DIRECTIONS = [PAST, FUTURE]
+
+ # Other endpoints use set_channel_and_chatable_with_access_check, but
+ # these endpoints require a standalone find because they need to be
+ # able to get deleted channels and recover them.
+ before_action :find_chatable, only: %i[enable_chat disable_chat]
+ before_action :find_chat_message,
+ only: %i[delete restore lookup_message edit_message rebake message_link]
+ before_action :set_channel_and_chatable_with_access_check,
+ except: %i[
+ respond
+ enable_chat
+ disable_chat
+ message_link
+ lookup_message
+ set_user_chat_status
+ dismiss_retention_reminder
+ flag
+ ]
+
+ def respond
+ render
+ end
+
+ def enable_chat
+ chat_channel = ChatChannel.with_deleted.find_by(chatable: @chatable)
+
+ guardian.ensure_can_see_chat_channel!(chat_channel) if chat_channel
+
+ if chat_channel && chat_channel.trashed?
+ chat_channel.recover!
+ elsif chat_channel
+ return render_json_error I18n.t("chat.already_enabled")
+ else
+ chat_channel = @chatable.chat_channel
+ guardian.ensure_can_see_chat_channel!(chat_channel)
+ end
+
+ success = chat_channel.save
+ if success && chat_channel.chatable_has_custom_fields?
+ @chatable.custom_fields[Chat::HAS_CHAT_ENABLED] = true
+ @chatable.save!
+ end
+
+ if success
+ membership = Chat::ChatChannelMembershipManager.new(channel).follow(user)
+ render_serialized(chat_channel, ChatChannelSerializer, membership: membership)
+ else
+ render_json_error(chat_channel)
+ end
+
+ Chat::ChatChannelMembershipManager.new(channel).follow(user)
+ end
+
+ def disable_chat
+ chat_channel = ChatChannel.with_deleted.find_by(chatable: @chatable)
+ guardian.ensure_can_see_chat_channel!(chat_channel)
+ return render json: success_json if chat_channel.trashed?
+ chat_channel.trash!(current_user)
+
+ success = chat_channel.save
+ if success
+ if chat_channel.chatable_has_custom_fields?
+ @chatable.custom_fields.delete(Chat::HAS_CHAT_ENABLED)
+ @chatable.save!
+ end
+
+ render json: success_json
+ else
+ render_json_error(chat_channel)
+ end
+ end
+
+ def create_message
+ raise Discourse::InvalidAccess if current_user.silenced?
+
+ Chat::ChatMessageRateLimiter.run!(current_user)
+
+ @user_chat_channel_membership =
+ Chat::ChatChannelMembershipManager.new(@chat_channel).find_for_user(
+ current_user,
+ following: true,
+ )
+ raise Discourse::InvalidAccess unless @user_chat_channel_membership
+
+ reply_to_msg_id = params[:in_reply_to_id]
+ if reply_to_msg_id
+ rm = ChatMessage.find(reply_to_msg_id)
+ raise Discourse::NotFound if rm.chat_channel_id != @chat_channel.id
+ end
+
+ content = params[:message]
+
+ chat_message_creator =
+ Chat::ChatMessageCreator.create(
+ chat_channel: @chat_channel,
+ user: current_user,
+ in_reply_to_id: reply_to_msg_id,
+ content: content,
+ staged_id: params[:staged_id],
+ upload_ids: params[:upload_ids],
+ )
+
+ return render_json_error(chat_message_creator.error) if chat_message_creator.failed?
+
+ @chat_channel.touch(:last_message_sent_at)
+ @user_chat_channel_membership.update(last_read_message_id: chat_message_creator.chat_message.id)
+
+ if @chat_channel.direct_message_channel?
+ # If any of the channel users is ignoring, muting, or preventing DMs from
+ # the current user then we shold not auto-follow the channel once again or
+ # publish the new channel.
+ user_ids_allowing_communication =
+ UserCommScreener.new(
+ acting_user: current_user,
+ target_user_ids: @chat_channel.user_chat_channel_memberships.pluck(:user_id),
+ ).allowing_actor_communication
+
+ if user_ids_allowing_communication.any?
+ @chat_channel
+ .user_chat_channel_memberships
+ .where(user_id: user_ids_allowing_communication)
+ .update_all(following: true)
+ ChatPublisher.publish_new_channel(
+ @chat_channel,
+ @chat_channel.chatable.users.where(id: user_ids_allowing_communication),
+ )
+ end
+ end
+
+ ChatPublisher.publish_user_tracking_state(
+ current_user,
+ @chat_channel.id,
+ chat_message_creator.chat_message.id,
+ )
+ render json: success_json
+ end
+
+ def edit_message
+ chat_message_updater =
+ Chat::ChatMessageUpdater.update(
+ guardian: guardian,
+ chat_message: @message,
+ new_content: params[:new_message],
+ upload_ids: params[:upload_ids] || [],
+ )
+
+ return render_json_error(chat_message_updater.error) if chat_message_updater.failed?
+
+ render json: success_json
+ end
+
+ def update_user_last_read
+ membership =
+ Chat::ChatChannelMembershipManager.new(@chat_channel).find_for_user(
+ current_user,
+ following: true,
+ )
+ raise Discourse::NotFound if membership.nil?
+
+ if membership.last_read_message_id && params[:message_id].to_i < membership.last_read_message_id
+ raise Discourse::InvalidParameters.new(:message_id)
+ end
+
+ unless ChatMessage.with_deleted.exists?(
+ chat_channel_id: @chat_channel.id,
+ id: params[:message_id],
+ )
+ raise Discourse::NotFound
+ end
+
+ membership.update!(last_read_message_id: params[:message_id])
+
+ Notification
+ .where(notification_type: Notification.types[:chat_mention])
+ .where(user: current_user)
+ .where(read: false)
+ .joins("INNER JOIN chat_mentions ON chat_mentions.notification_id = notifications.id")
+ .joins("INNER JOIN chat_messages ON chat_mentions.chat_message_id = chat_messages.id")
+ .where("chat_messages.id <= ?", params[:message_id].to_i)
+ .where("chat_messages.chat_channel_id = ?", @chat_channel.id)
+ .update_all(read: true)
+
+ ChatPublisher.publish_user_tracking_state(current_user, @chat_channel.id, params[:message_id])
+
+ render json: success_json
+ end
+
+ def messages
+ page_size = params[:page_size]&.to_i || 1000
+ direction = params[:direction].to_s
+ message_id = params[:message_id]
+ if page_size > 50 ||
+ (
+ message_id.blank? ^ direction.blank? &&
+ (direction.present? && !CHAT_DIRECTIONS.include?(direction))
+ )
+ raise Discourse::InvalidParameters
+ end
+
+ messages = preloaded_chat_message_query.where(chat_channel: @chat_channel)
+ messages = messages.with_deleted if guardian.can_moderate_chat?(@chatable)
+
+ if message_id.present?
+ condition = direction == PAST ? "<" : ">"
+ messages = messages.where("id #{condition} ?", message_id.to_i)
+ end
+
+ # NOTE: This order is reversed when we return the ChatView below if the direction
+ # is not FUTURE.
+ order = direction == FUTURE ? "ASC" : "DESC"
+ messages = messages.order("created_at #{order}, id #{order}").limit(page_size).to_a
+
+ can_load_more_past = nil
+ can_load_more_future = nil
+
+ if direction == FUTURE
+ can_load_more_future = messages.size == page_size
+ elsif direction == PAST
+ can_load_more_past = messages.size == page_size
+ else
+ # When direction is blank, we'll return the latest messages.
+ can_load_more_future = false
+ can_load_more_past = messages.size == page_size
+ end
+
+ chat_view =
+ ChatView.new(
+ chat_channel: @chat_channel,
+ chat_messages: direction == FUTURE ? messages : messages.reverse,
+ user: current_user,
+ can_load_more_past: can_load_more_past,
+ can_load_more_future: can_load_more_future,
+ )
+ render_serialized(chat_view, ChatViewSerializer, root: false)
+ end
+
+ def react
+ params.require(%i[message_id emoji react_action])
+ guardian.ensure_can_react!
+
+ Chat::ChatMessageReactor.new(current_user, @chat_channel).react!(
+ message_id: params[:message_id],
+ react_action: params[:react_action].to_sym,
+ emoji: params[:emoji],
+ )
+
+ render json: success_json
+ end
+
+ def delete
+ guardian.ensure_can_delete_chat!(@message, @chatable)
+
+ updated = @message.trash!(current_user)
+ if updated
+ ChatPublisher.publish_delete!(@chat_channel, @message)
+ render json: success_json
+ else
+ render_json_error(@message)
+ end
+ end
+
+ def restore
+ chat_channel = @message.chat_channel
+ guardian.ensure_can_restore_chat!(@message, chat_channel.chatable)
+ updated = @message.recover!
+ if updated
+ ChatPublisher.publish_restore!(chat_channel, @message)
+ render json: success_json
+ else
+ render_json_error(@message)
+ end
+ end
+
+ def rebake
+ guardian.ensure_can_rebake_chat_message!(@message)
+ @message.rebake!(invalidate_oneboxes: true)
+ render json: success_json
+ end
+
+ def message_link
+ return render_404 if @message.blank? || @message.deleted_at.present?
+ return render_404 if @message.chat_channel.blank?
+ set_channel_and_chatable_with_access_check(chat_channel_id: @message.chat_channel_id)
+ render json:
+ success_json.merge(
+ chat_channel_id: @chat_channel.id,
+ chat_channel_title: @chat_channel.title(current_user),
+ )
+ end
+
+ def lookup_message
+ set_channel_and_chatable_with_access_check(chat_channel_id: @message.chat_channel_id)
+
+ messages = preloaded_chat_message_query.where(chat_channel: @chat_channel)
+ messages = messages.with_deleted if guardian.can_moderate_chat?(@chatable)
+
+ past_messages =
+ messages
+ .where("created_at < ?", @message.created_at)
+ .order(created_at: :desc)
+ .limit(PAST_MESSAGE_LIMIT)
+
+ future_messages =
+ messages
+ .where("created_at > ?", @message.created_at)
+ .order(created_at: :asc)
+ .limit(FUTURE_MESSAGE_LIMIT)
+
+ can_load_more_past = past_messages.count == PAST_MESSAGE_LIMIT
+ can_load_more_future = future_messages.count == FUTURE_MESSAGE_LIMIT
+ messages = [past_messages.reverse, [@message], future_messages].reduce([], :concat)
+ chat_view =
+ ChatView.new(
+ chat_channel: @chat_channel,
+ chat_messages: messages,
+ user: current_user,
+ can_load_more_past: can_load_more_past,
+ can_load_more_future: can_load_more_future,
+ )
+ render_serialized(chat_view, ChatViewSerializer, root: false)
+ end
+
+ def set_user_chat_status
+ params.require(:chat_enabled)
+
+ current_user.user_option.update(chat_enabled: params[:chat_enabled])
+ render json: { chat_enabled: current_user.user_option.chat_enabled }
+ end
+
+ def invite_users
+ params.require(:user_ids)
+
+ users =
+ User
+ .includes(:groups)
+ .joins(:user_option)
+ .where(user_options: { chat_enabled: true })
+ .not_suspended
+ .where(id: params[:user_ids])
+ users.each do |user|
+ guardian = Guardian.new(user)
+ if guardian.can_chat?(user) && guardian.can_see_chat_channel?(@chat_channel)
+ data = {
+ message: "chat.invitation_notification",
+ chat_channel_id: @chat_channel.id,
+ chat_channel_title: @chat_channel.title(user),
+ chat_channel_slug: @chat_channel.slug,
+ invited_by_username: current_user.username,
+ }
+ data[:chat_message_id] = params[:chat_message_id] if params[:chat_message_id]
+ user.notifications.create(
+ notification_type: Notification.types[:chat_invitation],
+ high_priority: true,
+ data: data.to_json,
+ )
+ end
+ end
+
+ render json: success_json
+ end
+
+ def dismiss_retention_reminder
+ params.require(:chatable_type)
+ guardian.ensure_can_chat!(current_user)
+ unless ChatChannel.chatable_types.include?(params[:chatable_type])
+ raise Discourse::InvalidParameters
+ end
+
+ field =
+ (
+ if ChatChannel.public_channel_chatable_types.include?(params[:chatable_type])
+ :dismissed_channel_retention_reminder
+ else
+ :dismissed_dm_retention_reminder
+ end
+ )
+ current_user.user_option.update(field => true)
+ render json: success_json
+ end
+
+ def quote_messages
+ params.require(:message_ids)
+
+ message_ids = params[:message_ids].map(&:to_i)
+ markdown =
+ ChatTranscriptService.new(
+ @chat_channel,
+ current_user,
+ messages_or_ids: message_ids,
+ ).generate_markdown
+ render json: success_json.merge(markdown: markdown)
+ end
+
+ def move_messages_to_channel
+ params.require(:message_ids)
+ params.require(:destination_channel_id)
+
+ raise Discourse::InvalidAccess if !guardian.can_move_chat_messages?(@chat_channel)
+ destination_channel =
+ Chat::ChatChannelFetcher.find_with_access_check(params[:destination_channel_id], guardian)
+
+ begin
+ message_ids = params[:message_ids].map(&:to_i)
+ moved_messages =
+ Chat::MessageMover.new(
+ acting_user: current_user,
+ source_channel: @chat_channel,
+ message_ids: message_ids,
+ ).move_to_channel(destination_channel)
+ rescue Chat::MessageMover::NoMessagesFound, Chat::MessageMover::InvalidChannel => err
+ return render_json_error(err.message)
+ end
+
+ render json:
+ success_json.merge(
+ destination_channel_id: destination_channel.id,
+ destination_channel_title: destination_channel.title(current_user),
+ first_moved_message_id: moved_messages.first.id,
+ )
+ end
+
+ def flag
+ RateLimiter.new(current_user, "flag_chat_message", 4, 1.minutes).performed!
+
+ permitted_params =
+ params.permit(
+ %i[chat_message_id flag_type_id message is_warning take_action queue_for_review],
+ )
+
+ chat_message =
+ ChatMessage.includes(:chat_channel, :revisions).find(permitted_params[:chat_message_id])
+
+ flag_type_id = permitted_params[:flag_type_id].to_i
+
+ if !ReviewableScore.types.values.include?(flag_type_id)
+ raise Discourse::InvalidParameters.new(:flag_type_id)
+ end
+
+ set_channel_and_chatable_with_access_check(chat_channel_id: chat_message.chat_channel_id)
+
+ result =
+ Chat::ChatReviewQueue.new.flag_message(chat_message, guardian, flag_type_id, permitted_params)
+
+ if result[:success]
+ render json: success_json
+ else
+ render_json_error(result[:errors])
+ end
+ end
+
+ def set_draft
+ if params[:data].present?
+ ChatDraft.find_or_initialize_by(user: current_user, chat_channel_id: @chat_channel.id).update(
+ data: params[:data],
+ )
+ else
+ ChatDraft.where(user: current_user, chat_channel_id: @chat_channel.id).destroy_all
+ end
+
+ render json: success_json
+ end
+
+ private
+
+ def preloaded_chat_message_query
+ query =
+ ChatMessage
+ .includes(in_reply_to: [:user, chat_webhook_event: [:incoming_chat_webhook]])
+ .includes(:revisions)
+ .includes(:user)
+ .includes(chat_webhook_event: :incoming_chat_webhook)
+ .includes(reactions: :user)
+ .includes(:bookmarks)
+ .includes(:uploads)
+ .includes(chat_channel: :chatable)
+
+ query = query.includes(user: :user_status) if SiteSetting.enable_user_status
+
+ query
+ end
+
+ def find_chatable
+ @chatable = Category.find_by(id: params[:chatable_id])
+ guardian.ensure_can_moderate_chat!(@chatable)
+ end
+
+ def find_chat_message
+ @message = preloaded_chat_message_query.with_deleted
+ @message = @message.where(chat_channel_id: params[:chat_channel_id]) if params[:chat_channel_id]
+ @message = @message.find_by(id: params[:message_id])
+ raise Discourse::NotFound unless @message
+ end
+end
diff --git a/plugins/chat/app/controllers/direct_messages_controller.rb b/plugins/chat/app/controllers/direct_messages_controller.rb
new file mode 100644
index 0000000000..8464b705e0
--- /dev/null
+++ b/plugins/chat/app/controllers/direct_messages_controller.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+class Chat::DirectMessagesController < Chat::ChatBaseController
+ # NOTE: For V1 of chat channel archiving and deleting we are not doing
+ # anything for DM channels, their behaviour will stay as is.
+ def create
+ guardian.ensure_can_chat!(current_user)
+ users = users_from_usernames(current_user, params)
+
+ begin
+ chat_channel =
+ Chat::DirectMessageChannelCreator.create!(acting_user: current_user, target_users: users)
+ render_serialized(
+ chat_channel,
+ ChatChannelSerializer,
+ root: "chat_channel",
+ membership: chat_channel.membership_for(current_user),
+ )
+ rescue Chat::DirectMessageChannelCreator::NotAllowed => err
+ render_json_error(err.message)
+ end
+ end
+
+ def index
+ guardian.ensure_can_chat!(current_user)
+ users = users_from_usernames(current_user, params)
+
+ direct_message = DirectMessage.for_user_ids(users.map(&:id).uniq)
+ if direct_message
+ chat_channel = ChatChannel.find_by(chatable: direct_message)
+ render_serialized(
+ chat_channel,
+ ChatChannelSerializer,
+ root: "chat_channel",
+ membership: chat_channel.membership_for(current_user),
+ )
+ else
+ render body: nil, status: 404
+ end
+ end
+
+ private
+
+ def users_from_usernames(current_user, params)
+ params.require(:usernames)
+
+ usernames =
+ (params[:usernames].is_a?(String) ? params[:usernames].split(",") : params[:usernames])
+
+ users = [current_user]
+ other_usernames = usernames - [current_user.username]
+ users.concat(User.where(username: other_usernames).to_a) if other_usernames.any?
+ users
+ end
+end
diff --git a/plugins/chat/app/controllers/emojis_controller.rb b/plugins/chat/app/controllers/emojis_controller.rb
new file mode 100644
index 0000000000..8d895e2bd7
--- /dev/null
+++ b/plugins/chat/app/controllers/emojis_controller.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+class Chat::EmojisController < Chat::ChatBaseController
+ def index
+ emojis = Emoji.all.group_by(&:group)
+ render json: MultiJson.dump(emojis)
+ end
+end
diff --git a/plugins/chat/app/controllers/incoming_chat_webhooks_controller.rb b/plugins/chat/app/controllers/incoming_chat_webhooks_controller.rb
new file mode 100644
index 0000000000..1cf4963621
--- /dev/null
+++ b/plugins/chat/app/controllers/incoming_chat_webhooks_controller.rb
@@ -0,0 +1,109 @@
+# frozen_string_literal: true
+
+class Chat::IncomingChatWebhooksController < ApplicationController
+ WEBHOOK_MAX_MESSAGE_LENGTH = 2000
+ WEBHOOK_MESSAGES_PER_MINUTE_LIMIT = 10
+
+ skip_before_action :verify_authenticity_token, :redirect_to_login_if_required
+
+ before_action :validate_payload
+
+ def create_message
+ debug_payload
+
+ process_webhook_payload(text: params[:text], key: params[:key])
+ end
+
+ # See https://api.slack.com/reference/messaging/payload for the
+ # slack message payload format. For now we only support the
+ # text param, which we preprocess lightly to remove the slack-isms
+ # in the formatting.
+ def create_message_slack_compatible
+ debug_payload
+
+ # See note in validate_payload on why this is needed
+ attachments =
+ if params[:payload].present?
+ payload = params[:payload]
+ if String === payload
+ payload = JSON.parse(payload)
+ payload.deep_symbolize_keys!
+ end
+ payload[:attachments]
+ else
+ params[:attachments]
+ end
+
+ if params[:text].present?
+ text = Chat::SlackCompatibility.process_text(params[:text])
+ else
+ text = Chat::SlackCompatibility.process_legacy_attachments(attachments)
+ end
+
+ process_webhook_payload(text: text, key: params[:key])
+ rescue JSON::ParserError
+ raise Discourse::InvalidParameters
+ end
+
+ private
+
+ def process_webhook_payload(text:, key:)
+ validate_message_length(text)
+ webhook = find_and_rate_limit_webhook(key)
+
+ chat_message_creator =
+ Chat::ChatMessageCreator.create(
+ chat_channel: webhook.chat_channel,
+ user: Discourse.system_user,
+ content: text,
+ incoming_chat_webhook: webhook,
+ )
+ if chat_message_creator.failed?
+ render_json_error(chat_message_creator.error)
+ else
+ render json: success_json
+ end
+ end
+
+ def find_and_rate_limit_webhook(key)
+ webhook = IncomingChatWebhook.includes(:chat_channel).find_by(key: key)
+ raise Discourse::NotFound unless webhook
+
+ # Rate limit to 10 messages per-minute. We can move to a site setting in the future if needed.
+ RateLimiter.new(
+ nil,
+ "incoming_chat_webhook_#{webhook.id}",
+ WEBHOOK_MESSAGES_PER_MINUTE_LIMIT,
+ 1.minute,
+ ).performed!
+ webhook
+ end
+
+ def validate_message_length(message)
+ return if message.length <= WEBHOOK_MAX_MESSAGE_LENGTH
+ raise Discourse::InvalidParameters.new(
+ "Body cannot be over #{WEBHOOK_MAX_MESSAGE_LENGTH} characters",
+ )
+ end
+
+ def validate_payload
+ params.require([:key])
+
+ # TODO (martin) It is not clear whether the :payload key is actually
+ # present in the webhooks sent from OpsGenie, so once it is confirmed
+ # in production what we are actually getting then we can remove this.
+ if !params[:text] && !params[:payload] && !params[:attachments]
+ raise Discourse::InvalidParameters
+ end
+ end
+
+ def debug_payload
+ return if !SiteSetting.chat_debug_webhook_payloads
+ Rails.logger.warn(
+ "Debugging chat webhook payload: " +
+ JSON.dump(
+ { payload: params[:payload], attachments: params[:attachments], text: params[:text] },
+ ),
+ )
+ end
+end
diff --git a/plugins/chat/app/core_ext/plugin_instance.rb b/plugins/chat/app/core_ext/plugin_instance.rb
new file mode 100644
index 0000000000..9e38199f2e
--- /dev/null
+++ b/plugins/chat/app/core_ext/plugin_instance.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+DiscoursePluginRegistry.define_register(:chat_markdown_features, Set)
+
+class Plugin::Instance
+ def chat
+ ChatPluginApiExtensions
+ end
+
+ module ChatPluginApiExtensions
+ def self.enable_markdown_feature(name)
+ DiscoursePluginRegistry.chat_markdown_features << name
+ end
+ end
+end
diff --git a/plugins/chat/app/jobs/regular/auto_join_channel_batch.rb b/plugins/chat/app/jobs/regular/auto_join_channel_batch.rb
new file mode 100644
index 0000000000..a4a11270de
--- /dev/null
+++ b/plugins/chat/app/jobs/regular/auto_join_channel_batch.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+module Jobs
+ class AutoJoinChannelBatch < ::Jobs::Base
+ def execute(args)
+ return "starts_at or ends_at missing" if args[:starts_at].blank? || args[:ends_at].blank?
+ start_user_id = args[:starts_at].to_i
+ end_user_id = args[:ends_at].to_i
+
+ return "End is higher than start" if end_user_id < start_user_id
+
+ channel =
+ ChatChannel.find_by(
+ id: args[:chat_channel_id],
+ auto_join_users: true,
+ chatable_type: "Category",
+ )
+
+ return if !channel
+
+ category = channel.chatable
+ return if !category
+
+ query_args = {
+ chat_channel_id: channel.id,
+ start: start_user_id,
+ end: end_user_id,
+ suspended_until: Time.zone.now,
+ last_seen_at: 3.months.ago,
+ channel_category: channel.chatable_id,
+ mode: UserChatChannelMembership.join_modes[:automatic],
+ }
+
+ new_member_ids = DB.query_single(create_memberships_query(category), query_args)
+
+ # Only do this if we are running auto-join for a single user, if we
+ # are doing it for many then we should do it after all batches are
+ # complete for the channel in Jobs::AutoManageChannelMemberships
+ if start_user_id == end_user_id
+ Chat::ChatChannelMembershipManager.new(channel).recalculate_user_count
+ end
+
+ ChatPublisher.publish_new_channel(channel.reload, User.where(id: new_member_ids))
+ end
+
+ private
+
+ def create_memberships_query(category)
+ query = <<~SQL
+ INSERT INTO user_chat_channel_memberships (user_id, chat_channel_id, following, created_at, updated_at, join_mode)
+ SELECT DISTINCT(users.id), :chat_channel_id, TRUE, NOW(), NOW(), :mode
+ FROM users
+ INNER JOIN user_options uo ON uo.user_id = users.id
+ LEFT OUTER JOIN user_chat_channel_memberships uccm ON
+ uccm.chat_channel_id = :chat_channel_id AND uccm.user_id = users.id
+ SQL
+
+ query += <<~SQL if category.read_restricted?
+ INNER JOIN group_users gu ON gu.user_id = users.id
+ LEFT OUTER JOIN category_groups cg ON cg.group_id = gu.group_id
+ SQL
+
+ query += <<~SQL
+ WHERE (users.id >= :start AND users.id <= :end) AND
+ users.staged IS FALSE AND users.active AND
+ NOT EXISTS(SELECT 1 FROM anonymous_users a WHERE a.user_id = users.id) AND
+ (suspended_till IS NULL OR suspended_till <= :suspended_until) AND
+ (last_seen_at > :last_seen_at) AND
+ uo.chat_enabled AND
+ uccm.id IS NULL
+ SQL
+
+ query += <<~SQL if category.read_restricted?
+ AND cg.category_id = :channel_category
+ SQL
+
+ query += "RETURNING user_chat_channel_memberships.user_id"
+ end
+ end
+end
diff --git a/plugins/chat/app/jobs/regular/auto_manage_channel_memberships.rb b/plugins/chat/app/jobs/regular/auto_manage_channel_memberships.rb
new file mode 100644
index 0000000000..6d579bc88e
--- /dev/null
+++ b/plugins/chat/app/jobs/regular/auto_manage_channel_memberships.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+module Jobs
+ class AutoManageChannelMemberships < ::Jobs::Base
+ def execute(args)
+ channel =
+ ChatChannel.includes(:chatable).find_by(
+ id: args[:chat_channel_id],
+ auto_join_users: true,
+ chatable_type: "Category",
+ )
+
+ return if !channel&.chatable
+
+ processed =
+ UserChatChannelMembership.where(
+ chat_channel: channel,
+ following: true,
+ join_mode: UserChatChannelMembership.join_modes[:automatic],
+ ).count
+
+ auto_join_query(channel).find_in_batches do |batch|
+ break if processed >= SiteSetting.max_chat_auto_joined_users
+
+ starts_at = batch.first.query_user_id
+ ends_at = batch.last.query_user_id
+
+ Jobs.enqueue(
+ :auto_join_channel_batch,
+ chat_channel_id: channel.id,
+ starts_at: starts_at,
+ ends_at: ends_at,
+ )
+
+ processed += batch.size
+ end
+
+ # The Jobs::AutoJoinChannelBatch job will only do this recalculation
+ # if it's operating on one user, so we need to make sure we do it for
+ # the channel here once this job is complete.
+ Chat::ChatChannelMembershipManager.new(channel).recalculate_user_count
+ end
+
+ private
+
+ def auto_join_query(channel)
+ category = channel.chatable
+
+ users =
+ User
+ .real
+ .activated
+ .not_suspended
+ .not_staged
+ .distinct
+ .select(:id, "users.id AS query_user_id")
+ .where("last_seen_at > ?", 3.months.ago)
+ .joins(:user_option)
+ .where(user_options: { chat_enabled: true })
+ .joins(<<~SQL)
+ LEFT OUTER JOIN user_chat_channel_memberships uccm
+ ON uccm.chat_channel_id = #{channel.id} AND
+ uccm.user_id = users.id
+ SQL
+ .where("uccm.id IS NULL")
+
+ if category.read_restricted?
+ users =
+ users
+ .joins(:group_users)
+ .joins("INNER JOIN category_groups cg ON cg.group_id = group_users.group_id")
+ .where("cg.category_id = ?", channel.chatable_id)
+ end
+
+ users
+ end
+ end
+end
diff --git a/plugins/chat/app/jobs/regular/chat_channel_archive.rb b/plugins/chat/app/jobs/regular/chat_channel_archive.rb
new file mode 100644
index 0000000000..c5eb878d33
--- /dev/null
+++ b/plugins/chat/app/jobs/regular/chat_channel_archive.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module Jobs
+ class ChatChannelArchive < ::Jobs::Base
+ sidekiq_options retry: false
+
+ def execute(args = {})
+ channel_archive = ::ChatChannelArchive.find_by(id: args[:chat_channel_archive_id])
+
+ # this should not really happen, but better to do this than throw an error
+ if channel_archive.blank?
+ Rails.logger.warn(
+ "Chat channel archive #{args[:chat_channel_archive_id]} could not be found, aborting archive job.",
+ )
+ return
+ end
+
+ return if channel_archive.complete?
+
+ DistributedMutex.synchronize(
+ "archive_chat_channel_#{channel_archive.chat_channel_id}",
+ validity: 20.minutes,
+ ) { Chat::ChatChannelArchiveService.new(channel_archive).execute }
+ end
+ end
+end
diff --git a/plugins/chat/app/jobs/regular/chat_channel_delete.rb b/plugins/chat/app/jobs/regular/chat_channel_delete.rb
new file mode 100644
index 0000000000..ac89be4db9
--- /dev/null
+++ b/plugins/chat/app/jobs/regular/chat_channel_delete.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+module Jobs
+ class ChatChannelDelete < ::Jobs::Base
+ def execute(args = {})
+ chat_channel = ::ChatChannel.with_deleted.find_by(id: args[:chat_channel_id])
+
+ # this should not really happen, but better to do this than throw an error
+ if chat_channel.blank?
+ Rails.logger.warn(
+ "Chat channel #{args[:chat_channel_id]} could not be found, aborting delete job.",
+ )
+ return
+ end
+
+ DistributedMutex.synchronize("delete_chat_channel_#{chat_channel.id}") do
+ Rails.logger.debug("Deleting webhooks and events for channel #{chat_channel.id}")
+ ChatMessage.transaction do
+ webhooks = IncomingChatWebhook.where(chat_channel: chat_channel)
+ ChatWebhookEvent.where(incoming_chat_webhook_id: webhooks.select(:id)).delete_all
+ webhooks.delete_all
+ end
+
+ Rails.logger.debug("Deleting drafts and memberships for channel #{chat_channel.id}")
+ ChatDraft.where(chat_channel: chat_channel).delete_all
+ UserChatChannelMembership.where(chat_channel: chat_channel).delete_all
+
+ Rails.logger.debug(
+ "Deleting chat messages, mentions, revisions, and uploads for channel #{chat_channel.id}",
+ )
+ ChatMessage.transaction do
+ chat_messages = ChatMessage.where(chat_channel: chat_channel)
+ message_ids = chat_messages.select(:id)
+ ChatMention.where(chat_message_id: message_ids).delete_all
+ ChatMessageRevision.where(chat_message_id: message_ids).delete_all
+ ChatMessageReaction.where(chat_message_id: message_ids).delete_all
+
+ # if the uploads are not used anywhere else they will be deleted
+ # by the CleanUpUploads job in core
+ ChatUpload.where(chat_message_id: message_ids).delete_all
+
+ # only the messages and the channel are Trashable, everything else gets
+ # permanently destroyed
+ chat_messages.update_all(
+ deleted_by_id: chat_channel.deleted_by_id,
+ deleted_at: Time.zone.now,
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/plugins/chat/app/jobs/regular/chat_notify_mentioned.rb b/plugins/chat/app/jobs/regular/chat_notify_mentioned.rb
new file mode 100644
index 0000000000..d6fa48e332
--- /dev/null
+++ b/plugins/chat/app/jobs/regular/chat_notify_mentioned.rb
@@ -0,0 +1,148 @@
+# frozen_string_literal: true
+
+module Jobs
+ class ChatNotifyMentioned < ::Jobs::Base
+ def execute(args = {})
+ @chat_message =
+ ChatMessage.includes(:user, :revisions, chat_channel: :chatable).find_by(
+ id: args[:chat_message_id],
+ )
+ if @chat_message.nil? ||
+ @chat_message.revisions.where("created_at > ?", args[:timestamp]).any?
+ return
+ end
+
+ @creator = @chat_message.user
+ @chat_channel = @chat_message.chat_channel
+ @already_notified_user_ids = args[:already_notified_user_ids] || []
+ user_ids_to_notify = args[:to_notify_ids_map] || {}
+ user_ids_to_notify.each { |mention_type, ids| process_mentions(ids, mention_type.to_sym) }
+ end
+
+ private
+
+ def get_memberships(user_ids)
+ query =
+ UserChatChannelMembership.includes(:user).where(
+ user_id: (user_ids - @already_notified_user_ids),
+ chat_channel_id: @chat_message.chat_channel_id,
+ )
+ query = query.where(following: true) if @chat_channel.public_channel?
+ query
+ end
+
+ def build_data_for(membership, identifier_type:)
+ data = {
+ chat_message_id: @chat_message.id,
+ chat_channel_id: @chat_channel.id,
+ mentioned_by_username: @creator.username,
+ is_direct_message_channel: @chat_channel.direct_message_channel?,
+ }
+
+ if !@is_direct_message_channel
+ data[:chat_channel_title] = @chat_channel.title(membership.user)
+ data[:chat_channel_slug] = @chat_channel.slug
+ end
+
+ return data if identifier_type == :direct_mentions
+
+ case identifier_type
+ when :here_mentions
+ data[:identifier] = "here"
+ when :global_mentions
+ data[:identifier] = "all"
+ else
+ data[:identifier] = identifier_type if identifier_type
+ data[:is_group_mention] = true
+ end
+
+ data
+ end
+
+ def build_payload_for(membership, identifier_type:)
+ payload = {
+ notification_type: Notification.types[:chat_mention],
+ username: @creator.username,
+ tag: Chat::ChatNotifier.push_notification_tag(:mention, @chat_channel.id),
+ excerpt: @chat_message.push_notification_excerpt,
+ post_url: "#{@chat_channel.relative_url}?messageId=#{@chat_message.id}",
+ }
+
+ translation_prefix =
+ (
+ if @chat_channel.direct_message_channel?
+ "discourse_push_notifications.popup.direct_message_chat_mention"
+ else
+ "discourse_push_notifications.popup.chat_mention"
+ end
+ )
+
+ translation_suffix = identifier_type == :direct_mentions ? "direct" : "other_type"
+ identifier_text =
+ case identifier_type
+ when :here_mentions
+ "@here"
+ when :global_mentions
+ "@all"
+ when :direct_mentions
+ ""
+ else
+ "@#{identifier_type}"
+ end
+
+ payload[:translated_title] = I18n.t(
+ "#{translation_prefix}.#{translation_suffix}",
+ username: @creator.username,
+ identifier: identifier_text,
+ channel: @chat_channel.title(membership.user),
+ )
+
+ payload
+ end
+
+ def create_notification!(membership, notification_data)
+ is_read = Chat::ChatNotifier.user_has_seen_message?(membership, @chat_message.id)
+
+ notification =
+ Notification.create!(
+ notification_type: Notification.types[:chat_mention],
+ user_id: membership.user_id,
+ high_priority: true,
+ data: notification_data.to_json,
+ read: is_read,
+ )
+ ChatMention.create!(
+ notification: notification,
+ user: membership.user,
+ chat_message: @chat_message,
+ )
+ end
+
+ def send_notifications(membership, notification_data, os_payload)
+ create_notification!(membership, notification_data)
+
+ if !membership.desktop_notifications_never? && !membership.muted?
+ MessageBus.publish(
+ "/chat/notification-alert/#{membership.user_id}",
+ os_payload,
+ user_ids: [membership.user_id],
+ )
+ end
+
+ if !membership.mobile_notifications_never? && !membership.muted?
+ PostAlerter.push_notification(membership.user, os_payload)
+ end
+ end
+
+ def process_mentions(user_ids, mention_type)
+ memberships = get_memberships(user_ids)
+
+ memberships.each do |membership|
+ notification_data = build_data_for(membership, identifier_type: mention_type)
+ payload = build_payload_for(membership, identifier_type: mention_type)
+
+ send_notifications(membership, notification_data, payload)
+ end
+ end
+ end
+end
diff --git a/plugins/chat/app/jobs/regular/chat_notify_watching.rb b/plugins/chat/app/jobs/regular/chat_notify_watching.rb
new file mode 100644
index 0000000000..e9b8805e88
--- /dev/null
+++ b/plugins/chat/app/jobs/regular/chat_notify_watching.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+module Jobs
+ class ChatNotifyWatching < ::Jobs::Base
+ def execute(args = {})
+ @chat_message =
+ ChatMessage.includes(:user, chat_channel: :chatable).find_by(id: args[:chat_message_id])
+ return if @chat_message.nil?
+
+ @creator = @chat_message.user
+ @chat_channel = @chat_message.chat_channel
+ @is_direct_message_channel = @chat_channel.direct_message_channel?
+
+ always_notification_level = UserChatChannelMembership::NOTIFICATION_LEVELS[:always]
+
+ members =
+ UserChatChannelMembership
+ .includes(user: :groups)
+ .joins(user: :user_option)
+ .where(user_option: { chat_enabled: true })
+ .where.not(user_id: args[:except_user_ids])
+ .where(chat_channel_id: @chat_channel.id)
+ .where(following: true)
+ .where(
+ "desktop_notification_level = ? OR mobile_notification_level = ?",
+ always_notification_level,
+ always_notification_level,
+ )
+ .merge(User.not_suspended)
+
+ if @is_direct_message_channel
+ UserCommScreener
+ .new(acting_user: @creator, target_user_ids: members.map(&:user_id))
+ .allowing_actor_communication
+ .each do |user_id|
+ send_notifications(members.find { |member| member.user_id == user_id })
+ end
+ else
+ members.each { |member| send_notifications(member) }
+ end
+ end
+
+ def send_notifications(membership)
+ user = membership.user
+ guardian = Guardian.new(user)
+ return unless guardian.can_chat?(user) && guardian.can_see_chat_channel?(@chat_channel)
+ return if Chat::ChatNotifier.user_has_seen_message?(membership, @chat_message.id)
+ return if online_user_ids.include?(user.id)
+
+ translation_key =
+ (
+ if @is_direct_message_channel
+ "discourse_push_notifications.popup.new_direct_chat_message"
+ else
+ "discourse_push_notifications.popup.new_chat_message"
+ end
+ )
+
+ translation_args = { username: @creator.username }
+ translation_args[:channel] = @chat_channel.title(user) unless @is_direct_message_channel
+
+ payload = {
+ username: @creator.username,
+ notification_type: Notification.types[:chat_message],
+ post_url: @chat_channel.relative_url,
+ translated_title: I18n.t(translation_key, translation_args),
+ tag: Chat::ChatNotifier.push_notification_tag(:message, @chat_channel.id),
+ excerpt: @chat_message.push_notification_excerpt,
+ }
+
+ if membership.desktop_notifications_always? && !membership.muted?
+ MessageBus.publish("/chat/notification-alert/#{user.id}", payload, user_ids: [user.id])
+ end
+
+ if membership.mobile_notifications_always? && !membership.muted?
+ PostAlerter.push_notification(user, payload)
+ end
+ end
+
+ def online_user_ids
+ @online_user_ids ||= PresenceChannel.new("/chat/online").user_ids
+ end
+ end
+end
diff --git a/plugins/chat/app/jobs/regular/process_chat_message.rb b/plugins/chat/app/jobs/regular/process_chat_message.rb
new file mode 100644
index 0000000000..612978bb23
--- /dev/null
+++ b/plugins/chat/app/jobs/regular/process_chat_message.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Jobs
+ class ProcessChatMessage < ::Jobs::Base
+ def execute(args = {})
+ DistributedMutex.synchronize(
+ "process_chat_message_#{args[:chat_message_id]}",
+ validity: 10.minutes,
+ ) do
+ chat_message = ChatMessage.find_by(id: args[:chat_message_id])
+ return if !chat_message
+ processor = Chat::ChatMessageProcessor.new(chat_message)
+ processor.run!
+
+ if args[:is_dirty] || processor.dirty?
+ chat_message.update(cooked: processor.html, cooked_version: ChatMessage::BAKED_VERSION)
+ ChatPublisher.publish_processed!(chat_message)
+ end
+ end
+ end
+ end
+end
diff --git a/plugins/chat/app/jobs/regular/update_channel_user_count.rb b/plugins/chat/app/jobs/regular/update_channel_user_count.rb
new file mode 100644
index 0000000000..0790a52e16
--- /dev/null
+++ b/plugins/chat/app/jobs/regular/update_channel_user_count.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Jobs
+ class UpdateChannelUserCount < Jobs::Base
+ def execute(args = {})
+ channel = ChatChannel.find_by(id: args[:chat_channel_id])
+ return if channel.blank?
+ return if !channel.user_count_stale
+
+ channel.update!(
+ user_count: ChatChannelMembershipsQuery.count(channel),
+ user_count_stale: false,
+ )
+
+ ChatPublisher.publish_chat_channel_metadata(channel)
+ end
+ end
+end
diff --git a/plugins/chat/app/jobs/scheduled/auto_join_users.rb b/plugins/chat/app/jobs/scheduled/auto_join_users.rb
new file mode 100644
index 0000000000..061a3dce8d
--- /dev/null
+++ b/plugins/chat/app/jobs/scheduled/auto_join_users.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Jobs
+ class AutoJoinUsers < ::Jobs::Scheduled
+ every 1.hour
+
+ def execute(_args)
+ ChatChannel
+ .where(auto_join_users: true)
+ .each do |channel|
+ Chat::ChatChannelMembershipManager.new(channel).enforce_automatic_channel_memberships
+ end
+ end
+ end
+end
diff --git a/plugins/chat/app/jobs/scheduled/delete_old_chat_messages.rb b/plugins/chat/app/jobs/scheduled/delete_old_chat_messages.rb
new file mode 100644
index 0000000000..0799915528
--- /dev/null
+++ b/plugins/chat/app/jobs/scheduled/delete_old_chat_messages.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module Jobs
+ class DeleteOldChatMessages < ::Jobs::Scheduled
+ daily at: 0.hours
+
+ def execute(args = {})
+ delete_public_channel_messages
+ delete_dm_channel_messages
+ end
+
+ def delete_public_channel_messages
+ return unless valid_day_value?(:chat_channel_retention_days)
+
+ ChatMessage
+ .in_public_channel
+ .with_deleted
+ .created_before(SiteSetting.chat_channel_retention_days.days.ago)
+ .in_batches(of: 200)
+ .each do |relation|
+ destroyed_ids = relation.destroy_all.pluck(:id)
+ reset_last_read_message_id(destroyed_ids)
+ delete_flags(destroyed_ids)
+ end
+ end
+
+ def delete_dm_channel_messages
+ return unless valid_day_value?(:chat_dm_retention_days)
+
+ ChatMessage
+ .in_dm_channel
+ .with_deleted
+ .created_before(SiteSetting.chat_dm_retention_days.days.ago)
+ .in_batches(of: 200)
+ .each do |relation|
+ destroyed_ids = relation.destroy_all.pluck(:id)
+ reset_last_read_message_id(destroyed_ids)
+ end
+ end
+
+ def valid_day_value?(setting_name)
+ (SiteSetting.public_send(setting_name) || 0).positive?
+ end
+
+ def reset_last_read_message_id(ids)
+ UserChatChannelMembership.where(last_read_message_id: ids).update_all(
+ last_read_message_id: nil,
+ )
+ end
+
+ def delete_flags(message_ids)
+ ReviewableChatMessage.where(target_id: message_ids).destroy_all
+ end
+ end
+end
diff --git a/plugins/chat/app/jobs/scheduled/email_chat_notifications.rb b/plugins/chat/app/jobs/scheduled/email_chat_notifications.rb
new file mode 100644
index 0000000000..470c6aa215
--- /dev/null
+++ b/plugins/chat/app/jobs/scheduled/email_chat_notifications.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Jobs
+ class EmailChatNotifications < ::Jobs::Scheduled
+ every 5.minutes
+
+ def execute(args = {})
+ return unless SiteSetting.chat_enabled
+
+ Chat::ChatMailer.send_unread_mentions_summary
+ end
+ end
+end
diff --git a/plugins/chat/app/jobs/scheduled/update_user_counts_for_chat_channels.rb b/plugins/chat/app/jobs/scheduled/update_user_counts_for_chat_channels.rb
new file mode 100644
index 0000000000..968982819f
--- /dev/null
+++ b/plugins/chat/app/jobs/scheduled/update_user_counts_for_chat_channels.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module Jobs
+ class UpdateUserCountsForChatChannels < ::Jobs::Scheduled
+ every 1.hour
+
+ # FIXME: This could become huge as the amount of channels grows, we
+ # need a different approach here. Perhaps we should only bother for
+ # channels updated or with new messages in the past N days? Perhaps
+ # we could update all the counts in a single query as well?
+ def execute(args = {})
+ ChatChannel
+ .where(status: %i[open closed])
+ .find_each { |chat_channel| set_user_count(chat_channel) }
+ end
+
+ def set_user_count(chat_channel)
+ current_count = chat_channel.user_count || 0
+ new_count = ChatChannelMembershipsQuery.count(chat_channel)
+ return if current_count == new_count
+
+ chat_channel.update(user_count: new_count, user_count_stale: false)
+ ChatPublisher.publish_chat_channel_metadata(chat_channel)
+ end
+ end
+end
diff --git a/plugins/chat/app/models/category_channel.rb b/plugins/chat/app/models/category_channel.rb
new file mode 100644
index 0000000000..e95c3d5cff
--- /dev/null
+++ b/plugins/chat/app/models/category_channel.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+class CategoryChannel < ChatChannel
+ alias_attribute :category, :chatable
+
+ delegate :read_restricted?, to: :category
+ delegate :url, to: :chatable, prefix: true
+
+ %i[category_channel? public_channel? chatable_has_custom_fields?].each do |name|
+ define_method(name) { true }
+ end
+
+ def allowed_group_ids
+ return if !read_restricted?
+
+ staff_groups = Group::AUTO_GROUPS.slice(:staff, :moderators, :admins).values
+ category.secure_group_ids.to_a.concat(staff_groups)
+ end
+
+ def title(_ = nil)
+ name.presence || category.name
+ end
+
+ def generate_auto_slug
+ return if self.slug.present?
+ self.slug = Slug.for(self.title.strip, "")
+ self.slug = "" if duplicate_slug?
+ end
+
+ def ensure_slug_ok
+ # if we don't unescape it first we strip the % from the encoded version
+ slug = SiteSetting.slug_generation_method == "encoded" ? CGI.unescape(self.slug) : self.slug
+ self.slug = Slug.for(slug, "", method: :encoded)
+
+ if self.slug.blank?
+ errors.add(:slug, :invalid)
+ elsif SiteSetting.slug_generation_method == "ascii" && !CGI.unescape(self.slug).ascii_only?
+ errors.add(:slug, I18n.t("chat.category_channel.errors.slug_contains_non_ascii_chars"))
+ elsif duplicate_slug?
+ errors.add(:slug, I18n.t("chat.category_channel.errors.is_already_in_use"))
+ end
+ end
+end
diff --git a/plugins/chat/app/models/chat_channel.rb b/plugins/chat/app/models/chat_channel.rb
new file mode 100644
index 0000000000..c2cbca6fd3
--- /dev/null
+++ b/plugins/chat/app/models/chat_channel.rb
@@ -0,0 +1,149 @@
+# frozen_string_literal: true
+
+class ChatChannel < ActiveRecord::Base
+ include Trashable
+
+ belongs_to :chatable, polymorphic: true
+ belongs_to :direct_message,
+ -> { where(chat_channels: { chatable_type: "DirectMessage" }) },
+ foreign_key: "chatable_id"
+
+ has_many :chat_messages
+ has_many :user_chat_channel_memberships
+
+ has_one :chat_channel_archive
+
+ enum :status, { open: 0, read_only: 1, closed: 2, archived: 3 }, scopes: false
+
+ validates :name,
+ length: {
+ maximum: Proc.new { SiteSetting.max_topic_title_length },
+ },
+ presence: true,
+ allow_nil: true
+ validate :ensure_slug_ok
+ before_validation :generate_auto_slug
+
+ scope :public_channels,
+ -> {
+ where(chatable_type: public_channel_chatable_types).where(
+ "categories.id IS NOT NULL",
+ ).joins(
+ "LEFT JOIN categories ON categories.id = chat_channels.chatable_id AND chat_channels.chatable_type = 'Category'",
+ )
+ }
+
+ delegate :empty?, to: :chat_messages, prefix: true
+
+ class << self
+ def public_channel_chatable_types
+ ["Category"]
+ end
+
+ def chatable_types
+ public_channel_chatable_types << "DirectMessage"
+ end
+ end
+
+ statuses.keys.each do |status|
+ define_method("#{status}!") { |acting_user| change_status(acting_user, status.to_sym) }
+ end
+
+ %i[
+ category_channel?
+ direct_message_channel?
+ public_channel?
+ chatable_has_custom_fields?
+ read_restricted?
+ ].each { |name| define_method(name) { false } }
+
+ %i[allowed_user_ids allowed_group_ids chatable_url].each { |name| define_method(name) { nil } }
+
+ def membership_for(user)
+ user_chat_channel_memberships.find_by(user: user)
+ end
+
+ def add(user)
+ Chat::ChatChannelMembershipManager.new(self).follow(user)
+ end
+
+ def remove(user)
+ Chat::ChatChannelMembershipManager.new(self).unfollow(user)
+ end
+
+ def status_name
+ I18n.t("chat.channel.statuses.#{self.status}")
+ end
+
+ def url
+ "#{Discourse.base_url}#{relative_url}"
+ end
+
+ def relative_url
+ "/chat/channel/#{self.id}/#{self.slug || "-"}"
+ end
+
+ private
+
+ def change_status(acting_user, target_status)
+ return if !Guardian.new(acting_user).can_change_channel_status?(self, target_status)
+ self.update!(status: target_status)
+ log_channel_status_change(acting_user: acting_user)
+ end
+
+ def log_channel_status_change(acting_user:)
+ DiscourseEvent.trigger(
+ :chat_channel_status_change,
+ channel: self,
+ old_status: status_previously_was,
+ new_status: status,
+ )
+
+ StaffActionLogger.new(acting_user).log_custom(
+ "chat_channel_status_change",
+ {
+ chat_channel_id: self.id,
+ chat_channel_name: self.name,
+ previous_value: status_previously_was,
+ new_value: status,
+ },
+ )
+
+ ChatPublisher.publish_channel_status(self)
+ end
+
+ def duplicate_slug?
+ ChatChannel.where(slug: self.slug).where.not(id: self.id).any?
+ end
+end
+
+# == Schema Information
+#
+# Table name: chat_channels
+#
+# id :bigint not null, primary key
+# chatable_id :integer not null
+# deleted_at :datetime
+# deleted_by_id :integer
+# featured_in_category_id :integer
+# delete_after_seconds :integer
+# chatable_type :string not null
+# created_at :datetime not null
+# updated_at :datetime not null
+# name :string
+# description :text
+# status :integer default("open"), not null
+# user_count :integer default(0), not null
+# last_message_sent_at :datetime not null
+# auto_join_users :boolean default(FALSE), not null
+# user_count_stale :boolean default(FALSE), not null
+# slug :string
+# type :string
+#
+# Indexes
+#
+# index_chat_channels_on_chatable_id (chatable_id)
+# index_chat_channels_on_chatable_id_and_chatable_type (chatable_id,chatable_type)
+# index_chat_channels_on_slug (slug)
+# index_chat_channels_on_status (status)
+#
diff --git a/plugins/chat/app/models/chat_channel_archive.rb b/plugins/chat/app/models/chat_channel_archive.rb
new file mode 100644
index 0000000000..e84cdb35e3
--- /dev/null
+++ b/plugins/chat/app/models/chat_channel_archive.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+class ChatChannelArchive < ActiveRecord::Base
+ belongs_to :chat_channel
+ belongs_to :archived_by, class_name: "User"
+
+ belongs_to :destination_topic, class_name: "Topic"
+
+ def complete?
+ self.archived_messages >= self.total_messages && self.chat_channel.chat_messages.count.zero?
+ end
+
+ def failed?
+ !complete? && self.archive_error.present?
+ end
+end
+
+# == Schema Information
+#
+# Table name: chat_channel_archives
+#
+# id :bigint not null, primary key
+# chat_channel_id :bigint not null
+# archived_by_id :integer not null
+# destination_topic_id :integer
+# destination_topic_title :string
+# destination_category_id :integer
+# destination_tags :string is an Array
+# total_messages :integer not null
+# archived_messages :integer default(0), not null
+# archive_error :string
+# created_at :datetime not null
+# updated_at :datetime not null
+#
+# Indexes
+#
+# index_chat_channel_archives_on_chat_channel_id (chat_channel_id)
+#
diff --git a/plugins/chat/app/models/chat_draft.rb b/plugins/chat/app/models/chat_draft.rb
new file mode 100644
index 0000000000..1d3781fa82
--- /dev/null
+++ b/plugins/chat/app/models/chat_draft.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class ChatDraft < ActiveRecord::Base
+ belongs_to :user
+ belongs_to :chat_channel
+end
+
+# == Schema Information
+#
+# Table name: chat_drafts
+#
+# id :bigint not null, primary key
+# user_id :integer not null
+# chat_channel_id :integer not null
+# data :text not null
+# created_at :datetime not null
+# updated_at :datetime not null
+#
diff --git a/plugins/chat/app/models/chat_mention.rb b/plugins/chat/app/models/chat_mention.rb
new file mode 100644
index 0000000000..e334acae47
--- /dev/null
+++ b/plugins/chat/app/models/chat_mention.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+class ChatMention < ActiveRecord::Base
+ belongs_to :user
+ belongs_to :chat_message
+ belongs_to :notification
+end
+
+# == Schema Information
+#
+# Table name: chat_mentions
+#
+# id :bigint not null, primary key
+# chat_message_id :integer not null
+# user_id :integer not null
+# notification_id :integer not null
+# created_at :datetime not null
+# updated_at :datetime not null
+#
+# Indexes
+#
+# chat_mentions_index (chat_message_id,user_id,notification_id) UNIQUE
+#
diff --git a/plugins/chat/app/models/chat_message.rb b/plugins/chat/app/models/chat_message.rb
new file mode 100644
index 0000000000..9b4159b3ef
--- /dev/null
+++ b/plugins/chat/app/models/chat_message.rb
@@ -0,0 +1,220 @@
+# frozen_string_literal: true
+
+class ChatMessage < ActiveRecord::Base
+ include Trashable
+ attribute :has_oneboxes, default: false
+
+ BAKED_VERSION = 2
+
+ belongs_to :chat_channel
+ belongs_to :user
+ belongs_to :in_reply_to, class_name: "ChatMessage"
+ belongs_to :last_editor, class_name: "User"
+ has_many :replies, class_name: "ChatMessage", foreign_key: "in_reply_to_id", dependent: :nullify
+ has_many :revisions, class_name: "ChatMessageRevision", dependent: :destroy
+ has_many :reactions, class_name: "ChatMessageReaction", dependent: :destroy
+ has_many :bookmarks, as: :bookmarkable, dependent: :destroy
+ has_many :chat_uploads, dependent: :destroy
+ has_many :uploads, through: :chat_uploads
+ has_one :chat_webhook_event, dependent: :destroy
+ has_one :chat_mention, dependent: :destroy
+
+ scope :in_public_channel,
+ -> {
+ joins(:chat_channel).where(
+ chat_channel: {
+ chatable_type: ChatChannel.public_channel_chatable_types,
+ },
+ )
+ }
+
+ scope :in_dm_channel,
+ -> { joins(:chat_channel).where(chat_channel: { chatable_type: "DirectMessage" }) }
+
+ scope :created_before, ->(date) { where("chat_messages.created_at < ?", date) }
+
+ before_save { self.last_editor_id ||= self.user_id }
+
+ def validate_message(has_uploads:)
+ WatchedWordsValidator.new(attributes: [:message]).validate(self)
+ Chat::DuplicateMessageValidator.new(self).validate
+
+ if !has_uploads && message_too_short?
+ self.errors.add(
+ :base,
+ I18n.t(
+ "chat.errors.minimum_length_not_met",
+ minimum: SiteSetting.chat_minimum_message_length,
+ ),
+ )
+ end
+ end
+
+ def attach_uploads(uploads)
+ return if uploads.blank?
+
+ now = Time.now
+ record_attrs =
+ uploads.map do |upload|
+ { upload_id: upload.id, chat_message_id: self.id, created_at: now, updated_at: now }
+ end
+ ChatUpload.insert_all!(record_attrs)
+ end
+
+ def excerpt
+ # just show the URL if the whole message is a URL, because we cannot excerpt oneboxes
+ return message if UrlHelper.relaxed_parse(message).is_a?(URI)
+
+ # upload-only messages are better represented as the filename
+ return uploads.first.original_filename if cooked.blank? && uploads.present?
+
+ # this may return blank for some complex things like quotes, that is acceptable
+ PrettyText.excerpt(cooked, 50, {})
+ end
+
+ def cooked_for_excerpt
+ (cooked.blank? && uploads.present?) ? "
+
+
+ <%- @grouped_messages.each do |chat_channel, messages| %>
+ <%- other_messages_count = messages.size - 2 %>
+
+
+
+
+
+ <%- if logo_url.blank? %>
+ <%= SiteSetting.title %>
+ <%- else %>
+
+
+ <%- end %>
+
+
+
+
+
+ <%= I18n.t("user_notifications.chat_summary.description", count: @messages.size) %>
+
+
+
+
+ <%- end %>
+
+
+ <%- messages.take(2).each do |chat_message| %>
+ <%- sender = chat_message.user %>
+ <%- sender_name = @display_usernames ? sender.username : sender.name %>
+
+
+
+ <%= chat_channel.title(@user) %>
+
+
+
+
+
+
+ <%= sender_name -%>
+
+
+ <%= I18n.l(@user_tz.to_local(chat_message.created_at), format: :long) -%>
+
+
+
+ <%- end %>
+
+ <%= email_excerpt(chat_message.cooked_for_excerpt) %>
+
+
+
+
+
+
+ <%- if other_messages_count <= 0 %>
+ <%= I18n.t("user_notifications.chat_summary.view_messages", count: messages.size)%>
+ <%- else %>
+ <%= I18n.t("user_notifications.chat_summary.view_more", count: other_messages_count)%>
+ <%- end %>
+
+
+
+
diff --git a/plugins/chat/app/views/user_notifications/chat_summary.text.erb b/plugins/chat/app/views/user_notifications/chat_summary.text.erb
new file mode 100644
index 0000000000..76955166f2
--- /dev/null
+++ b/plugins/chat/app/views/user_notifications/chat_summary.text.erb
@@ -0,0 +1,15 @@
+<%- site_link = raw(@markdown_linker.create(@site_name, '/')) %>
+<%= t('user_notifications.chat_summary.description', count: @messages.size,) %>
+<%= raw(@markdown_linker.create(t("user_notifications.chat_summary.view_messages", count: @messages.size), "/chat")) %>
+<%- if @unsubscribe_link %>
+ <%= raw(t :'user_notifications.chat_summary.unsubscribe',
+ site_link: site_link,
+ email_preferences_link: @markdown_linker.create(t('user_notifications.chat_summary.your_chat_settings'), @preferences_path),
+ unsubscribe_link: @markdown_linker.create(t('user_notifications.digest.click_here'), @unsubscribe_link)) %>
+<%- else %>
+ <%= raw(t :'user_notifications.chat_summary.unsubscribe_no_link',
+ site_link: site_link,
+ email_preferences_link: @markdown_linker.create(t('user_notifications.chat_summary.your_chat_settings'), @preferences_path)) %>
+<%- end %>
+
+<%= raw(@markdown_linker.references) %>
diff --git a/plugins/chat/assets/javascripts/discourse/adapters/chat-message.js b/plugins/chat/assets/javascripts/discourse/adapters/chat-message.js
new file mode 100644
index 0000000000..51f4b36f25
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/adapters/chat-message.js
@@ -0,0 +1,22 @@
+import RESTAdapter from "discourse/adapters/rest";
+
+export default class ChatMessage extends RESTAdapter {
+ pathFor(store, type, findArgs) {
+ if (findArgs.targetMessageId) {
+ return `/chat/lookup/${findArgs.targetMessageId}.json?chat_channel_id=${findArgs.channelId}`;
+ }
+
+ let path = `/chat/${findArgs.channelId}/messages.json?page_size=${findArgs.pageSize}`;
+ if (findArgs.messageId) {
+ path += `&message_id=${findArgs.messageId}`;
+ }
+ if (findArgs.direction) {
+ path += `&direction=${findArgs.direction}`;
+ }
+ return path;
+ }
+
+ apiNameFor() {
+ return "chat-message";
+ }
+}
diff --git a/plugins/chat/assets/javascripts/discourse/admin-chat-route-map.js b/plugins/chat/assets/javascripts/discourse/admin-chat-route-map.js
new file mode 100644
index 0000000000..a1e74d0708
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/admin-chat-route-map.js
@@ -0,0 +1,7 @@
+export default {
+ resource: "admin.adminPlugins",
+ path: "/plugins",
+ map() {
+ this.route("chat");
+ },
+};
diff --git a/plugins/chat/assets/javascripts/discourse/chat-route-map.js b/plugins/chat/assets/javascripts/discourse/chat-route-map.js
new file mode 100644
index 0000000000..a4e03558a2
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/chat-route-map.js
@@ -0,0 +1,25 @@
+export default function () {
+ this.route("chat", { path: "/chat" }, function () {
+ this.route(
+ "channel",
+ { path: "/channel/:channelId/:channelTitle" },
+ function () {
+ this.route("info", { path: "/info" }, function () {
+ this.route("about", { path: "/about" });
+ this.route("members", { path: "/members" });
+ this.route("settings", { path: "/settings" });
+ });
+ }
+ );
+
+ this.route("draft-channel", { path: "/draft-channel" });
+ this.route("browse", { path: "/browse" }, function () {
+ this.route("all", { path: "/all" });
+ this.route("closed", { path: "/closed" });
+ this.route("open", { path: "/open" });
+ this.route("archived", { path: "/archived" });
+ });
+ this.route("message", { path: "/message/:messageId" });
+ this.route("channelByName", { path: "/chat_channels/:channelName" });
+ });
+}
diff --git a/plugins/chat/assets/javascripts/discourse/components/channels-list.js b/plugins/chat/assets/javascripts/discourse/components/channels-list.js
new file mode 100644
index 0000000000..ae145525a0
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/components/channels-list.js
@@ -0,0 +1,110 @@
+import { bind } from "discourse-common/utils/decorators";
+import Component from "@ember/component";
+import { action, computed } from "@ember/object";
+import { schedule } from "@ember/runloop";
+import { inject as service } from "@ember/service";
+import { and, empty, reads } from "@ember/object/computed";
+
+export default class ChannelsList extends Component {
+ @service chat;
+ @service router;
+ @service chatStateManager;
+ tagName = "";
+ inSidebar = false;
+ toggleSection = null;
+ @reads("chat.publicChannels.[]") publicChannels;
+ @reads("chat.directMessageChannels.[]") directMessageChannels;
+ @empty("publicChannels") publicChannelsEmpty;
+ @and("site.mobileView", "showDirectMessageChannels")
+ showMobileDirectMessageButton;
+
+ @computed("canCreateDirectMessageChannel")
+ get createDirectMessageChannelLabel() {
+ if (!this.canCreateDirectMessageChannel) {
+ return "chat.direct_messages.cannot_create";
+ }
+
+ return "chat.direct_messages.new";
+ }
+
+ @computed("canCreateDirectMessageChannel", "directMessageChannels")
+ get showDirectMessageChannels() {
+ return (
+ this.canCreateDirectMessageChannel ||
+ this.directMessageChannels?.length > 0
+ );
+ }
+
+ get canCreateDirectMessageChannel() {
+ return this.chat.userCanDirectMessage;
+ }
+
+ @computed("directMessageChannels.@each.last_message_sent_at")
+ get sortedDirectMessageChannels() {
+ if (!this.directMessageChannels?.length) {
+ return [];
+ }
+
+ return this.chat.truncateDirectMessageChannels(
+ this.chat.sortDirectMessageChannels(this.directMessageChannels)
+ );
+ }
+
+ @computed("inSidebar")
+ get publicChannelClasses() {
+ return `channels-list-container public-channels ${
+ this.inSidebar ? "collapsible-sidebar-section" : ""
+ }`;
+ }
+
+ @computed(
+ "publicChannelsEmpty",
+ "currentUser.{staff,has_joinable_public_channels}"
+ )
+ get displayPublicChannels() {
+ if (this.publicChannelsEmpty) {
+ return (
+ this.currentUser?.staff ||
+ this.currentUser?.has_joinable_public_channels
+ );
+ }
+
+ return true;
+ }
+
+ @computed("inSidebar")
+ get directMessageChannelClasses() {
+ return `channels-list-container direct-message-channels ${
+ this.inSidebar ? "collapsible-sidebar-section" : ""
+ }`;
+ }
+
+ @action
+ toggleChannelSection(section) {
+ this.toggleSection(section);
+ }
+
+ didRender() {
+ this._super(...arguments);
+
+ schedule("afterRender", this._applyScrollPosition);
+ }
+
+ @action
+ storeScrollPosition() {
+ const scroller = document.querySelector(".channels-list");
+ if (scroller) {
+ const scrollTop = scroller.scrollTop || 0;
+ this.session.set("channels-list-position", scrollTop);
+ }
+ }
+
+ @bind
+ _applyScrollPosition() {
+ const data = this.session.get("channels-list-position");
+ if (data) {
+ const scroller = document.querySelector(".channels-list");
+ scroller.scrollTo(0, data);
+ }
+ }
+}
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-browse-view.js b/plugins/chat/assets/javascripts/discourse/components/chat-browse-view.js
new file mode 100644
index 0000000000..a453ad360f
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-browse-view.js
@@ -0,0 +1,99 @@
+import { INPUT_DELAY } from "discourse-common/config/environment";
+import Component from "@ember/component";
+import { action } from "@ember/object";
+import { tracked } from "@glimmer/tracking";
+import { inject as service } from "@ember/service";
+import ChatApi from "discourse/plugins/chat/discourse/lib/chat-api";
+import discourseDebounce from "discourse-common/lib/debounce";
+import { bind } from "discourse-common/utils/decorators";
+import showModal from "discourse/lib/show-modal";
+
+const TABS = ["all", "open", "closed", "archived"];
+const PER_PAGE = 20;
+
+export default class ChatBrowseView extends Component {
+ @service router;
+ @tracked isLoading = false;
+ @tracked channels = [];
+ tagName = "";
+
+ tabs = TABS;
+ offset = 0;
+ canLoadMore = true;
+
+ didReceiveAttrs() {
+ this._super(...arguments);
+
+ this.channels = [];
+ this.canLoadMore = true;
+ this.offset = 0;
+ this.fetchChannels();
+ }
+
+ async fetchChannels(params) {
+ if (this.isLoading || !this.canLoadMore) {
+ return;
+ }
+
+ this.isLoading = true;
+
+ try {
+ const results = await ChatApi.chatChannels({
+ limit: PER_PAGE,
+ offset: this.offset,
+ status: this.status,
+ filter: this.filter,
+ ...params,
+ });
+
+ if (results.length) {
+ this.channels.pushObjects(results);
+ }
+
+ if (results.length < PER_PAGE) {
+ this.canLoadMore = false;
+ }
+ } finally {
+ this.offset = this.offset + PER_PAGE;
+ this.isLoading = false;
+ }
+ }
+
+ get chatProgressBarContainer() {
+ return document.querySelector("#chat-progress-bar-container");
+ }
+
+ @action
+ onScroll() {
+ if (this.isLoading) {
+ return;
+ }
+
+ discourseDebounce(this, this.fetchChannels, INPUT_DELAY);
+ }
+
+ @action
+ debouncedFiltering(event) {
+ discourseDebounce(
+ this,
+ this.filterChannels,
+ event.target.value,
+ INPUT_DELAY
+ );
+ }
+
+ @action
+ createChannel() {
+ showModal("create-channel");
+ }
+
+ @bind
+ filterChannels(filter) {
+ this.canLoadMore = true;
+ this.filter = filter;
+ this.channels = [];
+ this.offset = 0;
+
+ this.fetchChannels();
+ }
+}
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-about-view.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-about-view.js
new file mode 100644
index 0000000000..3b4d038645
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-about-view.js
@@ -0,0 +1,19 @@
+import Component from "@ember/component";
+import { action } from "@ember/object";
+import { inject as service } from "@ember/service";
+
+export default class ChatChannelAboutView extends Component {
+ @service chat;
+ tagName = "";
+ channel = null;
+ onEditChatChannelTitle = null;
+ onEditChatChannelDescription = null;
+ isLoading = false;
+
+ @action
+ afterMembershipToggle() {
+ this.chat.forceRefreshChannels().then(() => {
+ this.chat.openChannel(this.channel);
+ });
+ }
+}
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-archive-modal-inner.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-archive-modal-inner.js
new file mode 100644
index 0000000000..2569653fac
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-archive-modal-inner.js
@@ -0,0 +1,114 @@
+import Component from "@ember/component";
+import I18n from "I18n";
+import discourseLater from "discourse-common/lib/later";
+import { isEmpty } from "@ember/utils";
+import discourseComputed from "discourse-common/utils/decorators";
+import { action } from "@ember/object";
+import { equal } from "@ember/object/computed";
+import { ajax } from "discourse/lib/ajax";
+import { inject as service } from "@ember/service";
+import { popupAjaxError } from "discourse/lib/ajax-error";
+import {
+ EXISTING_TOPIC_SELECTION,
+ NEW_TOPIC_SELECTION,
+} from "discourse/plugins/chat/discourse/components/chat-to-topic-selector";
+import { CHANNEL_STATUSES } from "discourse/plugins/chat/discourse/models/chat-channel";
+import { htmlSafe } from "@ember/template";
+
+export default Component.extend({
+ chat: service(),
+ tagName: "",
+ chatChannel: null,
+
+ selection: "newTopic",
+ newTopic: equal("selection", NEW_TOPIC_SELECTION),
+ existingTopic: equal("selection", EXISTING_TOPIC_SELECTION),
+
+ saving: false,
+
+ topicTitle: null,
+ categoryId: null,
+ tags: null,
+ selectedTopicId: null,
+
+ @action
+ archiveChannel() {
+ this.set("saving", true);
+ return ajax({
+ url: `/chat/chat_channels/${this.chatChannel.id}/archive.json`,
+ type: "PUT",
+ data: this._data(),
+ })
+ .then(() => {
+ this.appEvents.trigger("modal-body:flash", {
+ text: I18n.t("chat.channel_archive.process_started"),
+ messageClass: "success",
+ });
+
+ this.chatChannel.set("status", CHANNEL_STATUSES.archived);
+
+ discourseLater(() => {
+ this.closeModal();
+ }, 3000);
+ })
+ .catch(popupAjaxError)
+ .finally(() => this.set("saving", false));
+ },
+
+ _data() {
+ const data = {
+ type: this.selection,
+ chat_channel_id: this.chatChannel.id,
+ };
+ if (this.newTopic) {
+ data.title = this.topicTitle;
+ data.category_id = this.categoryId;
+ data.tags = this.tags;
+ }
+ if (this.existingTopic) {
+ data.topic_id = this.selectedTopicId;
+ }
+ return data;
+ },
+
+ @discourseComputed("saving", "selectedTopicId", "topicTitle", "selection")
+ buttonDisabled(saving, selectedTopicId, topicTitle) {
+ if (saving) {
+ return true;
+ }
+ if (
+ this.newTopic &&
+ (!topicTitle ||
+ topicTitle.length < this.siteSettings.min_topic_title_length ||
+ topicTitle.length > this.siteSettings.max_topic_title_length)
+ ) {
+ return true;
+ }
+
+ if (this.existingTopic && isEmpty(selectedTopicId)) {
+ return true;
+ }
+ return false;
+ },
+
+ @discourseComputed()
+ instructionLabels() {
+ const labels = {};
+ labels[NEW_TOPIC_SELECTION] = I18n.t(
+ "chat.selection.new_topic.instructions_channel_archive"
+ );
+ labels[EXISTING_TOPIC_SELECTION] = I18n.t(
+ "chat.selection.existing_topic.instructions_channel_archive"
+ );
+ return labels;
+ },
+
+ @discourseComputed()
+ instructionsText() {
+ return htmlSafe(
+ I18n.t("chat.channel_archive.instructions", {
+ channelTitle: this.chatChannel.escapedTitle,
+ })
+ );
+ },
+});
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-archive-status.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-archive-status.js
new file mode 100644
index 0000000000..6f006cddd0
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-archive-status.js
@@ -0,0 +1,81 @@
+import Component from "@ember/component";
+import { htmlSafe } from "@ember/template";
+import I18n from "I18n";
+import { popupAjaxError } from "discourse/lib/ajax-error";
+import { ajax } from "discourse/lib/ajax";
+import getURL from "discourse-common/lib/get-url";
+import { action } from "@ember/object";
+import discourseComputed from "discourse-common/utils/decorators";
+
+export default Component.extend({
+ channel: null,
+ tagName: "",
+
+ @discourseComputed(
+ "channel.status",
+ "channel.archived_messages",
+ "channel.total_messages",
+ "channel.archive_failed"
+ )
+ channelArchiveFailedMessage() {
+ return htmlSafe(
+ I18n.t("chat.channel_status.archive_failed", {
+ completed: this.channel.archived_messages,
+ total: this.channel.total_messages,
+ topic_url: this._getTopicUrl(),
+ })
+ );
+ },
+
+ @discourseComputed(
+ "channel.status",
+ "channel.archived_messages",
+ "channel.total_messages",
+ "channel.archive_completed"
+ )
+ channelArchiveCompletedMessage() {
+ return htmlSafe(
+ I18n.t("chat.channel_status.archive_completed", {
+ topic_url: this._getTopicUrl(),
+ })
+ );
+ },
+
+ @action
+ retryArchive() {
+ return ajax({
+ url: `/chat/chat_channels/${this.channel.id}/retry_archive.json`,
+ type: "PUT",
+ })
+ .then(() => {
+ this.channel.set("archive_failed", false);
+ })
+ .catch(popupAjaxError);
+ },
+
+ didInsertElement() {
+ this._super(...arguments);
+ if (this.currentUser.admin) {
+ this.messageBus.subscribe("/chat/channel-archive-status", (busData) => {
+ if (busData.chat_channel_id === this.channel.id) {
+ this.channel.setProperties({
+ archive_failed: busData.archive_failed,
+ archive_completed: busData.archive_completed,
+ archived_messages: busData.archived_messages,
+ archive_topic_id: busData.archive_topic_id,
+ total_messages: busData.total_messages,
+ });
+ }
+ });
+ }
+ },
+
+ willDestroyElement() {
+ this._super(...arguments);
+ this.messageBus.unsubscribe("/chat/channel-archive-status");
+ },
+
+ _getTopicUrl() {
+ return getURL(`/t/-/${this.channel.archive_topic_id}`);
+ },
+});
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-card.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-card.js
new file mode 100644
index 0000000000..3392fe9f05
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-card.js
@@ -0,0 +1,13 @@
+import Component from "@ember/component";
+import { action } from "@ember/object";
+import { inject as service } from "@ember/service";
+
+export default class ChatChannelCard extends Component {
+ @service chat;
+ tagName = "";
+
+ @action
+ afterMembershipToggle() {
+ this.chat.forceRefreshChannels();
+ }
+}
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-chooser-header.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-chooser-header.js
new file mode 100644
index 0000000000..68622e7560
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-chooser-header.js
@@ -0,0 +1,3 @@
+import ComboBoxSelectBoxHeaderComponent from "select-kit/components/combo-box/combo-box-header";
+
+export default ComboBoxSelectBoxHeaderComponent.extend({});
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-chooser-row.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-chooser-row.js
new file mode 100644
index 0000000000..50e8d0b319
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-chooser-row.js
@@ -0,0 +1,5 @@
+import SelectKitRowComponent from "select-kit/components/select-kit/select-kit-row";
+
+export default SelectKitRowComponent.extend({
+ classNames: ["chat-channel-chooser-row"],
+});
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-chooser.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-chooser.js
new file mode 100644
index 0000000000..94ab9da6d8
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-chooser.js
@@ -0,0 +1,14 @@
+import ComboBoxComponent from "select-kit/components/combo-box";
+
+export default ComboBoxComponent.extend({
+ pluginApiIdentifiers: ["chat-channel-chooser"],
+ classNames: ["chat-channel-chooser"],
+
+ selectKitOptions: {
+ headerComponent: "chat-channel-chooser-header",
+ },
+
+ modifyComponentForRow() {
+ return "chat-channel-chooser-row";
+ },
+});
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-delete-modal-inner.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-delete-modal-inner.js
new file mode 100644
index 0000000000..3f38523f18
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-delete-modal-inner.js
@@ -0,0 +1,68 @@
+import Component from "@ember/component";
+import { isEmpty } from "@ember/utils";
+import I18n from "I18n";
+import discourseComputed from "discourse-common/utils/decorators";
+import { action } from "@ember/object";
+import { ajax } from "discourse/lib/ajax";
+import { inject as service } from "@ember/service";
+import { popupAjaxError } from "discourse/lib/ajax-error";
+import discourseLater from "discourse-common/lib/later";
+import { htmlSafe } from "@ember/template";
+
+export default Component.extend({
+ chat: service(),
+ router: service(),
+ tagName: "",
+ chatChannel: null,
+ channelNameConfirmation: null,
+ deleting: false,
+ confirmed: false,
+
+ @discourseComputed("deleting", "channelNameConfirmation", "confirmed")
+ buttonDisabled(deleting, channelNameConfirmation, confirmed) {
+ if (deleting || confirmed) {
+ return true;
+ }
+
+ if (
+ isEmpty(channelNameConfirmation) ||
+ channelNameConfirmation.toLowerCase() !==
+ this.chatChannel.title.toLowerCase()
+ ) {
+ return true;
+ }
+ return false;
+ },
+
+ @action
+ deleteChannel() {
+ this.set("deleting", true);
+ return ajax(`/chat/chat_channels/${this.chatChannel.id}.json`, {
+ method: "DELETE",
+ data: { channel_name_confirmation: this.channelNameConfirmation },
+ })
+ .then(() => {
+ this.set("confirmed", true);
+ this.appEvents.trigger("modal-body:flash", {
+ text: I18n.t("chat.channel_delete.process_started"),
+ messageClass: "success",
+ });
+
+ discourseLater(() => {
+ this.closeModal();
+ this.router.transitionTo("chat");
+ }, 3000);
+ })
+ .catch(popupAjaxError)
+ .finally(() => this.set("deleting", false));
+ },
+
+ @discourseComputed()
+ instructionsText() {
+ return htmlSafe(
+ I18n.t("chat.channel_delete.instructions", {
+ name: this.chatChannel.escapedTitle,
+ })
+ );
+ },
+});
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-leave-btn.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-leave-btn.js
new file mode 100644
index 0000000000..3347b5e236
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-leave-btn.js
@@ -0,0 +1,25 @@
+import discourseComputed from "discourse-common/utils/decorators";
+import Component from "@ember/component";
+import { equal } from "@ember/object/computed";
+import { inject as service } from "@ember/service";
+import { CHATABLE_TYPES } from "discourse/plugins/chat/discourse/models/chat-channel";
+
+export default Component.extend({
+ tagName: "",
+ channel: null,
+ chat: service(),
+
+ isDirectMessageRow: equal(
+ "channel.chatable_type",
+ CHATABLE_TYPES.directMessageChannel
+ ),
+
+ @discourseComputed("isDirectMessageRow")
+ leaveChatTitleKey(isDirectMessageRow) {
+ if (isDirectMessageRow) {
+ return "chat.direct_messages.leave";
+ } else {
+ return "chat.leave";
+ }
+ },
+});
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-members-view.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-members-view.js
new file mode 100644
index 0000000000..49907dbd68
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-members-view.js
@@ -0,0 +1,113 @@
+import { isEmpty } from "@ember/utils";
+import { INPUT_DELAY } from "discourse-common/config/environment";
+import Component from "@ember/component";
+import { action } from "@ember/object";
+import { schedule } from "@ember/runloop";
+import ChatApi from "discourse/plugins/chat/discourse/lib/chat-api";
+import discourseDebounce from "discourse-common/lib/debounce";
+
+const LIMIT = 50;
+
+export default class ChatChannelMembersView extends Component {
+ tagName = "";
+ channel = null;
+ members = null;
+ isSearchFocused = false;
+ isFetchingMembers = false;
+ onlineUsers = null;
+ offset = 0;
+ filter = null;
+ inputSelector = "channel-members-view__search-input";
+ canLoadMore = true;
+
+ didInsertElement() {
+ this._super(...arguments);
+
+ if (!this.channel || this.channel.isDraft) {
+ return;
+ }
+
+ this._focusSearch();
+ this.set("members", []);
+ this.fetchMembers();
+
+ this.appEvents.on("chat:refresh-channel-members", this, "onFilterMembers");
+ }
+
+ willDestroyElement() {
+ this._super(...arguments);
+ this.appEvents.off("chat:refresh-channel-members", this, "onFilterMembers");
+ }
+
+ get chatProgressBarContainer() {
+ return document.querySelector("#chat-progress-bar-container");
+ }
+
+ @action
+ onFilterMembers(username) {
+ this.set("filter", username);
+ this.set("offset", 0);
+ this.set("canLoadMore", true);
+
+ discourseDebounce(
+ this,
+ this.fetchMembers,
+ this.filter,
+ this.offset,
+ INPUT_DELAY
+ );
+ }
+
+ @action
+ loadMore() {
+ if (!this.canLoadMore) {
+ return;
+ }
+
+ discourseDebounce(
+ this,
+ this.fetchMembers,
+ this.filter,
+ this.offset,
+ INPUT_DELAY
+ );
+ }
+
+ fetchMembersHandler(id, params = {}) {
+ return ChatApi.chatChannelMemberships(id, params);
+ }
+
+ fetchMembers(filter = null, offset = 0) {
+ this.set("isFetchingMembers", true);
+
+ return this.fetchMembersHandler(this.channel.id, {
+ username: filter,
+ offset,
+ })
+ .then((response) => {
+ if (this.offset === 0) {
+ this.set("members", []);
+ }
+
+ if (isEmpty(response)) {
+ this.set("canLoadMore", false);
+ } else {
+ this.set("offset", this.offset + LIMIT);
+ this.members.pushObjects(response);
+ }
+ })
+ .finally(() => {
+ this.set("isFetchingMembers", false);
+ });
+ }
+
+ _focusSearch() {
+ if (this.capabilities.isIpadOS || this.site.mobileView) {
+ return;
+ }
+
+ schedule("afterRender", () => {
+ document.getElementsByClassName(this.inputSelector)[0]?.focus();
+ });
+ }
+}
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-preview-card.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-preview-card.js
new file mode 100644
index 0000000000..23be8f9a67
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-preview-card.js
@@ -0,0 +1,26 @@
+import Component from "@ember/component";
+import { isEmpty } from "@ember/utils";
+import { action, computed } from "@ember/object";
+import { readOnly } from "@ember/object/computed";
+import { inject as service } from "@ember/service";
+
+export default class ChatChannelPreviewCard extends Component {
+ @service chat;
+ tagName = "";
+
+ channel = null;
+
+ @readOnly("channel.isOpen") showJoinButton;
+
+ @computed("channel.description")
+ get hasDescription() {
+ return !isEmpty(this.channel.description);
+ }
+
+ @action
+ afterMembershipToggle() {
+ this.chat.forceRefreshChannels().then(() => {
+ this.chat.openChannel(this.channel);
+ });
+ }
+}
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-row.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-row.js
new file mode 100644
index 0000000000..ae3b281645
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-row.js
@@ -0,0 +1,83 @@
+import Component from "@ember/component";
+import I18n from "I18n";
+import discourseComputed from "discourse-common/utils/decorators";
+import { equal } from "@ember/object/computed";
+import { inject as service } from "@ember/service";
+import { CHATABLE_TYPES } from "discourse/plugins/chat/discourse/models/chat-channel";
+
+export default Component.extend({
+ tagName: "",
+ router: service(),
+ chat: service(),
+ channel: null,
+ isDirectMessageRow: equal(
+ "channel.chatable_type",
+ CHATABLE_TYPES.directMessageChannel
+ ),
+ options: null,
+
+ didInsertElement() {
+ this._super(...arguments);
+
+ if (this.isDirectMessageRow) {
+ this.channel.chatable.users[0].trackStatus();
+ }
+ },
+
+ willDestroyElement() {
+ this._super(...arguments);
+
+ if (this.isDirectMessageRow) {
+ this.channel.chatable.users[0].stopTrackingStatus();
+ }
+ },
+
+ @discourseComputed(
+ "channel.id",
+ "chat.activeChannel.id",
+ "router.currentRouteName"
+ )
+ active(channelId, activeChannelId, currentRouteName) {
+ return (
+ currentRouteName?.startsWith("chat.channel") &&
+ channelId === activeChannelId
+ );
+ },
+
+ @discourseComputed("active", "channel.{id,muted}", "channel.focused")
+ rowClassNames(active, channel, focused) {
+ const classes = ["chat-channel-row", `chat-channel-${channel.id}`];
+ if (active) {
+ classes.push("active");
+ }
+ if (focused) {
+ classes.push("focused");
+ }
+ if (channel.current_user_membership.muted) {
+ classes.push("muted");
+ }
+ return classes.join(" ");
+ },
+
+ @discourseComputed(
+ "isDirectMessageRow",
+ "channel.chatable.users.[]",
+ "channel.chatable.users.@each.status"
+ )
+ showUserStatus(isDirectMessageRow) {
+ return !!(
+ isDirectMessageRow &&
+ this.channel.chatable.users.length === 1 &&
+ this.channel.chatable.users[0].status
+ );
+ },
+
+ @discourseComputed("channel.chatable_type")
+ leaveChatTitle() {
+ if (this.channel.isDirectMessageChannel) {
+ return I18n.t("chat.direct_messages.leave");
+ } else {
+ return I18n.t("chat.channel_settings.leave_channel");
+ }
+ },
+});
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-selection-row.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-selection-row.js
new file mode 100644
index 0000000000..07d4e9b6c5
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-selection-row.js
@@ -0,0 +1,22 @@
+import Component from "@ember/component";
+import discourseComputed from "discourse-common/utils/decorators";
+import { action } from "@ember/object";
+
+export default Component.extend({
+ tagName: "",
+
+ @discourseComputed("model", "model.focused")
+ rowClassNames(model, focused) {
+ return `chat-channel-selection-row ${focused ? "focused" : ""} ${
+ this.model.user ? "user-row" : "channel-row"
+ }`;
+ },
+
+ @action
+ handleClick(event) {
+ if (this.onClick) {
+ this.onClick(this.model);
+ event.preventDefault();
+ }
+ },
+});
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-selector-modal-inner.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-selector-modal-inner.js
new file mode 100644
index 0000000000..bfd46d64c0
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-selector-modal-inner.js
@@ -0,0 +1,181 @@
+import Component from "@ember/component";
+import { action } from "@ember/object";
+import ChatChannel from "discourse/plugins/chat/discourse/models/chat-channel";
+import { ajax } from "discourse/lib/ajax";
+import { bind } from "discourse-common/utils/decorators";
+import { schedule } from "@ember/runloop";
+import { inject as service } from "@ember/service";
+import { popupAjaxError } from "discourse/lib/ajax-error";
+import discourseDebounce from "discourse-common/lib/debounce";
+import { INPUT_DELAY } from "discourse-common/config/environment";
+import { isPresent } from "@ember/utils";
+
+export default Component.extend({
+ chat: service(),
+ tagName: "",
+ filter: "",
+ channels: null,
+ searchIndex: 0,
+ loading: false,
+
+ init() {
+ this._super(...arguments);
+ this.appEvents.on("chat-channel-selector-modal:close", this.close);
+ this.getInitialChannels();
+ },
+
+ didInsertElement() {
+ this._super(...arguments);
+ document.addEventListener("keyup", this.onKeyUp);
+ document
+ .getElementById("chat-channel-selector-modal-inner")
+ ?.addEventListener("mouseover", this.mouseover);
+ document.getElementById("chat-channel-selector-input")?.focus();
+ },
+
+ willDestroyElement() {
+ this._super(...arguments);
+ this.appEvents.off("chat-channel-selector-modal:close", this.close);
+ document.removeEventListener("keyup", this.onKeyUp);
+ document
+ .getElementById("chat-channel-selector-modal-inner")
+ ?.removeEventListener("mouseover", this.mouseover);
+ },
+
+ @bind
+ mouseover(e) {
+ if (e.target.classList.contains("chat-channel-selection-row")) {
+ let channel;
+ const id = parseInt(e.target.dataset.id, 10);
+ if (e.target.classList.contains("channel-row")) {
+ channel = this.channels.findBy("id", id);
+ } else {
+ channel = this.channels.find((c) => c.user && c.id === id);
+ }
+ channel?.set("focused", true);
+ this.channels.forEach((c) => {
+ if (c !== channel) {
+ c.set("focused", false);
+ }
+ });
+ }
+ },
+
+ @bind
+ onKeyUp(e) {
+ if (e.key === "Enter") {
+ let focusedChannel = this.channels.find((c) => c.focused);
+ this.switchChannel(focusedChannel);
+ e.preventDefault();
+ } else if (e.key === "ArrowDown") {
+ this.arrowNavigateChannels("down");
+ e.preventDefault();
+ } else if (e.key === "ArrowUp") {
+ this.arrowNavigateChannels("up");
+ e.preventDefault();
+ }
+ },
+
+ arrowNavigateChannels(direction) {
+ const indexOfFocused = this.channels.findIndex((c) => c.focused);
+ if (indexOfFocused > -1) {
+ const nextIndex = direction === "down" ? 1 : -1;
+ const nextChannel = this.channels[indexOfFocused + nextIndex];
+ if (nextChannel) {
+ this.channels[indexOfFocused].set("focused", false);
+ nextChannel.set("focused", true);
+ }
+ } else {
+ this.channels[0].set("focused", true);
+ }
+
+ schedule("afterRender", () => {
+ let focusedChannel = document.querySelector(
+ "#chat-channel-selector-modal-inner .chat-channel-selection-row.focused"
+ );
+ focusedChannel?.scrollIntoView({ block: "nearest", inline: "start" });
+ });
+ },
+
+ @action
+ switchChannel(channel) {
+ if (channel.user) {
+ return this.fetchOrCreateChannelForUser(channel).then((response) => {
+ this.chat
+ .startTrackingChannel(ChatChannel.create(response.chat_channel))
+ .then((newlyTracked) => {
+ this.chat.openChannel(newlyTracked);
+ this.close();
+ });
+ });
+ } else {
+ this.chat.openChannel(channel);
+ this.close();
+ }
+ },
+
+ @action
+ search(value) {
+ if (isPresent(value?.trim())) {
+ discourseDebounce(
+ this,
+ this.fetchChannelsFromServer,
+ value?.trim(),
+ INPUT_DELAY
+ );
+ } else {
+ discourseDebounce(this, this.getInitialChannels, INPUT_DELAY);
+ }
+ },
+
+ @action
+ fetchChannelsFromServer(filter) {
+ this.setProperties({
+ loading: true,
+ searchIndex: this.searchIndex + 1,
+ });
+ const thisSearchIndex = this.searchIndex;
+ ajax("/chat/chat_channels/search", { data: { filter } })
+ .then((searchModel) => {
+ if (this.searchIndex === thisSearchIndex) {
+ this.set("searchModel", searchModel);
+ const channels = searchModel.public_channels.concat(
+ searchModel.direct_message_channels,
+ searchModel.users
+ );
+ channels.forEach((c) => {
+ if (c.username) {
+ c.user = true; // This is used by the `chat-channel-selection-row` component
+ }
+ });
+ this.setProperties({
+ channels: channels.map((channel) => ChatChannel.create(channel)),
+ loading: false,
+ });
+ this.focusFirstChannel(this.channels);
+ }
+ })
+ .catch(popupAjaxError);
+ },
+
+ @action
+ getInitialChannels() {
+ return this.chat.getChannelsWithFilter(this.filter).then((channels) => {
+ this.focusFirstChannel(channels);
+ this.set("channels", channels);
+ });
+ },
+
+ @action
+ fetchOrCreateChannelForUser(user) {
+ return ajax("/chat/direct_messages/create.json", {
+ method: "POST",
+ data: { usernames: [user.username] },
+ }).catch(popupAjaxError);
+ },
+
+ focusFirstChannel(channels) {
+ channels.forEach((c) => c.set("focused", false));
+ channels[0]?.set("focused", true);
+ },
+});
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-settings-row.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-settings-row.js
new file mode 100644
index 0000000000..9918af7a34
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-settings-row.js
@@ -0,0 +1,40 @@
+import Component from "@ember/component";
+import discourseComputed from "discourse-common/utils/decorators";
+import I18n from "I18n";
+import { action } from "@ember/object";
+import { inject as service } from "@ember/service";
+
+const NOTIFICATION_LEVELS = [
+ { name: I18n.t("chat.notification_levels.never"), value: "never" },
+ { name: I18n.t("chat.notification_levels.mention"), value: "mention" },
+ { name: I18n.t("chat.notification_levels.always"), value: "always" },
+];
+
+const MUTED_OPTIONS = [
+ { name: I18n.t("chat.settings.muted_on"), value: true },
+ { name: I18n.t("chat.settings.muted_off"), value: false },
+];
+
+export default Component.extend({
+ channel: null,
+ loading: false,
+ showSaveSuccess: false,
+ notificationLevels: NOTIFICATION_LEVELS,
+ mutedOptions: MUTED_OPTIONS,
+ chat: service(),
+ router: service(),
+
+ didInsertElement() {
+ this._super(...arguments);
+ },
+
+ @discourseComputed("channel.chatable_type")
+ chatChannelClass(channelType) {
+ return `${channelType.toLowerCase()}-chat-channel`;
+ },
+
+ @action
+ previewChannel() {
+ this.chat.openChannel(this.channel);
+ },
+});
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-settings-view.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-settings-view.js
new file mode 100644
index 0000000000..8ca9eda17a
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-settings-view.js
@@ -0,0 +1,135 @@
+import Component from "@ember/component";
+import { action, computed } from "@ember/object";
+import { inject as service } from "@ember/service";
+import ChatApi from "discourse/plugins/chat/discourse/lib/chat-api";
+import showModal from "discourse/lib/show-modal";
+import I18n from "I18n";
+import { camelize } from "@ember/string";
+import discourseLater from "discourse-common/lib/later";
+
+const NOTIFICATION_LEVELS = [
+ { name: I18n.t("chat.notification_levels.never"), value: "never" },
+ { name: I18n.t("chat.notification_levels.mention"), value: "mention" },
+ { name: I18n.t("chat.notification_levels.always"), value: "always" },
+];
+
+const MUTED_OPTIONS = [
+ { name: I18n.t("chat.settings.muted_on"), value: true },
+ { name: I18n.t("chat.settings.muted_off"), value: false },
+];
+
+export default class ChatChannelSettingsView extends Component {
+ @service chat;
+ @service router;
+ @service dialog;
+ tagName = "";
+ channel = null;
+
+ notificationLevels = NOTIFICATION_LEVELS;
+ mutedOptions = MUTED_OPTIONS;
+ isSavingNotificationSetting = false;
+ savedDesktopNotificationLevel = false;
+ savedMobileNotificationLevel = false;
+ savedMuted = false;
+
+ _updateAutoJoinUsers(value) {
+ return ChatApi.modifyChatChannel(this.channel.id, {
+ auto_join_users: value,
+ })
+ .then((chatChannel) => {
+ this.channel.set("auto_join_users", chatChannel.auto_join_users);
+ })
+ .catch((event) => {
+ if (event.jqXHR?.responseJSON?.errors) {
+ this.flash(event.jqXHR.responseJSON.errors.join("\n"), "error");
+ }
+ });
+ }
+
+ @action
+ saveNotificationSettings(key, value) {
+ if (this.channel[key] === value) {
+ return;
+ }
+
+ const camelizedKey = camelize(`saved_${key}`);
+ this.set(camelizedKey, false);
+
+ const settings = {};
+ settings[key] = value;
+ return ChatApi.updateChatChannelNotificationsSettings(
+ this.channel.id,
+ settings
+ )
+ .then((membership) => {
+ this.channel.current_user_membership.setProperties({
+ muted: membership.muted,
+ desktop_notification_level: membership.desktop_notification_level,
+ mobile_notification_level: membership.mobile_notification_level,
+ });
+ this.set(camelizedKey, true);
+ })
+ .finally(() => {
+ discourseLater(() => {
+ if (this.isDestroying || this.isDestroyed) {
+ return;
+ }
+
+ this.set(camelizedKey, false);
+ }, 2000);
+ });
+ }
+
+ @computed(
+ "siteSettings.chat_allow_archiving_channels",
+ "channel.{isArchived,isReadOnly}"
+ )
+ get canArchiveChannel() {
+ return (
+ this.siteSettings.chat_allow_archiving_channels &&
+ !this.channel.isArchived &&
+ !this.channel.isReadOnly
+ );
+ }
+
+ @computed("channel.isCategoryChannel")
+ get autoJoinAvailable() {
+ return (
+ this.siteSettings.max_chat_auto_joined_users > 0 &&
+ this.channel.isCategoryChannel
+ );
+ }
+
+ @action
+ onArchiveChannel() {
+ const controller = showModal("chat-channel-archive-modal");
+ controller.set("chatChannel", this.channel);
+ }
+
+ @action
+ onDeleteChannel() {
+ const controller = showModal("chat-channel-delete-modal");
+ controller.set("chatChannel", this.channel);
+ }
+
+ @action
+ onToggleChannelState() {
+ const controller = showModal("chat-channel-toggle");
+ controller.set("chatChannel", this.channel);
+ }
+
+ @action
+ onDisableAutoJoinUsers() {
+ this._updateAutoJoinUsers(false);
+ }
+
+ @action
+ onEnableAutoJoinUsers() {
+ this.dialog.confirm({
+ message: I18n.t("chat.settings.auto_join_users_warning", {
+ category: this.channel.chatable.name,
+ }),
+ didConfirm: () => this._updateAutoJoinUsers(true),
+ });
+ }
+}
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-status.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-status.js
new file mode 100644
index 0000000000..f6048b21b9
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-status.js
@@ -0,0 +1,57 @@
+import discourseComputed from "discourse-common/utils/decorators";
+import I18n from "I18n";
+import Component from "@ember/component";
+import {
+ CHANNEL_STATUSES,
+ channelStatusIcon,
+ channelStatusName,
+} from "discourse/plugins/chat/discourse/models/chat-channel";
+
+export default Component.extend({
+ tagName: "",
+ channel: null,
+ format: null,
+
+ init() {
+ this._super(...arguments);
+ if (!["short", "long"].includes(this.format)) {
+ this.set("format", "long");
+ }
+ },
+
+ @discourseComputed("channel.status")
+ channelStatusMessage(channelStatus) {
+ if (channelStatus === CHANNEL_STATUSES.open) {
+ return null;
+ }
+
+ if (this.format === "long") {
+ return this._longStatusMessage(channelStatus);
+ } else {
+ return this._shortStatusMessage(channelStatus);
+ }
+ },
+
+ @discourseComputed("channel.status")
+ channelStatusIcon(channelStatus) {
+ return channelStatusIcon(channelStatus);
+ },
+
+ _shortStatusMessage(channelStatus) {
+ return channelStatusName(channelStatus);
+ },
+
+ _longStatusMessage(channelStatus) {
+ switch (channelStatus) {
+ case CHANNEL_STATUSES.closed:
+ return I18n.t("chat.channel_status.closed_header");
+ break;
+ case CHANNEL_STATUSES.readOnly:
+ return I18n.t("chat.channel_status.read_only_header");
+ break;
+ case CHANNEL_STATUSES.archived:
+ return I18n.t("chat.channel_status.archived_header");
+ break;
+ }
+ },
+});
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-title.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-title.js
new file mode 100644
index 0000000000..fc261ddcfa
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-title.js
@@ -0,0 +1,23 @@
+import Component from "@ember/component";
+import { htmlSafe } from "@ember/template";
+import { computed } from "@ember/object";
+import { gt, reads } from "@ember/object/computed";
+
+export default class ChatChannelTitle extends Component {
+ tagName = "";
+ channel = null;
+ unreadIndicator = false;
+
+ @reads("channel.chatable.users.[]") users;
+ @gt("users.length", 1) multiDm;
+
+ @computed("users")
+ get usernames() {
+ return this.users.mapBy("username").join(", ");
+ }
+
+ @computed("channel.chatable.color")
+ get channelColorStyle() {
+ return htmlSafe(`color: #${this.channel.chatable.color}`);
+ }
+}
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-toggle-view.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-toggle-view.js
new file mode 100644
index 0000000000..87c0cfd9b2
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-toggle-view.js
@@ -0,0 +1,62 @@
+import Component from "@ember/component";
+import { htmlSafe } from "@ember/template";
+import { CHANNEL_STATUSES } from "discourse/plugins/chat/discourse/models/chat-channel";
+import I18n from "I18n";
+import { action, computed } from "@ember/object";
+import { ajax } from "discourse/lib/ajax";
+import { inject as service } from "@ember/service";
+import { popupAjaxError } from "discourse/lib/ajax-error";
+
+export default class ChatChannelToggleView extends Component {
+ @service chat;
+ @service router;
+ tagName = "";
+ channel = null;
+ onStatusChange = null;
+
+ @computed("channel.isClosed")
+ get buttonLabel() {
+ if (this.channel.isClosed) {
+ return "chat.channel_settings.open_channel";
+ } else {
+ return "chat.channel_settings.close_channel";
+ }
+ }
+
+ @computed("channel.isClosed")
+ get instructions() {
+ if (this.channel.isClosed) {
+ return htmlSafe(I18n.t("chat.channel_open.instructions"));
+ } else {
+ return htmlSafe(I18n.t("chat.channel_close.instructions"));
+ }
+ }
+
+ @computed("channel.isClosed")
+ get modalTitle() {
+ if (this.channel.isClosed) {
+ return "chat.channel_open.title";
+ } else {
+ return "chat.channel_close.title";
+ }
+ }
+
+ @action
+ changeChannelStatus() {
+ const status = this.channel.isClosed
+ ? CHANNEL_STATUSES.open
+ : CHANNEL_STATUSES.closed;
+
+ return ajax(`/chat/chat_channels/${this.channel.id}/change_status.json`, {
+ method: "PUT",
+ data: { status },
+ })
+ .then(() => {
+ this.channel.set("status", status);
+ })
+ .catch(popupAjaxError)
+ .finally(() => {
+ this.onStatusChange?.(this.channel);
+ });
+ }
+}
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-channel-unread-indicator.js b/plugins/chat/assets/javascripts/discourse/components/chat-channel-unread-indicator.js
new file mode 100644
index 0000000000..f24cd64a31
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-channel-unread-indicator.js
@@ -0,0 +1,46 @@
+import discourseComputed from "discourse-common/utils/decorators";
+import Component from "@ember/component";
+import { equal, gt } from "@ember/object/computed";
+import { CHATABLE_TYPES } from "discourse/plugins/chat/discourse/models/chat-channel";
+
+export default Component.extend({
+ tagName: "",
+ channel: null,
+
+ isDirectMessage: equal(
+ "channel.chatable_type",
+ CHATABLE_TYPES.directMessageChannel
+ ),
+
+ hasUnread: gt("unreadCount", 0),
+
+ @discourseComputed(
+ "currentUser.chat_channel_tracking_state.@each.{unread_count,unread_mentions}",
+ "channel.id"
+ )
+ channelTrackingState(state, channelId) {
+ return state?.[channelId];
+ },
+
+ @discourseComputed(
+ "channelTrackingState.unread_mentions",
+ "channel",
+ "isDirectMessage"
+ )
+ isUrgent(unreadMentions, channel, isDirectMessage) {
+ if (!channel) {
+ return;
+ }
+
+ return isDirectMessage || unreadMentions > 0;
+ },
+
+ @discourseComputed("channelTrackingState.unread_count", "channel")
+ unreadCount(unreadCount, channel) {
+ if (!channel) {
+ return;
+ }
+
+ return unreadCount || 0;
+ },
+});
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-composer-dropdown.js b/plugins/chat/assets/javascripts/discourse/components/chat-composer-dropdown.js
new file mode 100644
index 0000000000..36dad78ae3
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-composer-dropdown.js
@@ -0,0 +1,7 @@
+import Component from "@ember/component";
+
+export default class ChatComposerDropdown extends Component {
+ tagName = "";
+ buttons = null;
+ isDisabled = false;
+}
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-composer-inline-buttons.js b/plugins/chat/assets/javascripts/discourse/components/chat-composer-inline-buttons.js
new file mode 100644
index 0000000000..88361c2939
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-composer-inline-buttons.js
@@ -0,0 +1,5 @@
+import Component from "@ember/component";
+
+export default class ChatComposerInlineButtons extends Component {
+ tagName = "";
+}
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-composer-message-details.js b/plugins/chat/assets/javascripts/discourse/components/chat-composer-message-details.js
new file mode 100644
index 0000000000..44494409ab
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-composer-message-details.js
@@ -0,0 +1,5 @@
+import Component from "@ember/component";
+
+export default Component.extend({
+ tagName: "",
+});
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-composer-upload.js b/plugins/chat/assets/javascripts/discourse/components/chat-composer-upload.js
new file mode 100644
index 0000000000..a2a89a2119
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-composer-upload.js
@@ -0,0 +1,25 @@
+import Component from "@ember/component";
+import discourseComputed from "discourse-common/utils/decorators";
+import { isImage } from "discourse/lib/uploads";
+
+export default Component.extend({
+ IMAGE_TYPE: "image",
+
+ tagName: "",
+ classNames: "chat-upload",
+ isDone: false,
+ upload: null,
+ onCancel: null,
+
+ @discourseComputed("upload.{original_filename,fileName}")
+ type(upload) {
+ if (isImage(upload.original_filename || upload.fileName)) {
+ return this.IMAGE_TYPE;
+ }
+ },
+
+ @discourseComputed("isDone", "upload.{original_filename,fileName}")
+ fileName(isDone, upload) {
+ return isDone ? upload.original_filename : upload.fileName;
+ },
+});
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-composer-uploads.js b/plugins/chat/assets/javascripts/discourse/components/chat-composer-uploads.js
new file mode 100644
index 0000000000..dba574f963
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-composer-uploads.js
@@ -0,0 +1,132 @@
+import Component from "@ember/component";
+import { clipboardHelpers } from "discourse/lib/utilities";
+import { action } from "@ember/object";
+import { inject as service } from "@ember/service";
+import UppyMediaOptimization from "discourse/lib/uppy-media-optimization-plugin";
+import discourseComputed, { bind } from "discourse-common/utils/decorators";
+import UppyUploadMixin from "discourse/mixins/uppy-upload";
+
+export default Component.extend(UppyUploadMixin, {
+ classNames: ["chat-composer-uploads"],
+ mediaOptimizationWorker: service(),
+ chatStateManager: service(),
+ id: "chat-composer-uploader",
+ type: "chat-composer",
+ uploads: null,
+ useMultipartUploadsIfAvailable: true,
+
+ init() {
+ this._super(...arguments);
+ this.setProperties({
+ uploads: [],
+ fileInputSelector: `#${this.fileUploadElementId}`,
+ });
+ this.appEvents.on("chat-composer:load-uploads", this, "_loadUploads");
+ },
+
+ didInsertElement() {
+ this._super(...arguments);
+ this.composerInputEl = document.querySelector(".chat-composer-input");
+ this.composerInputEl?.addEventListener("paste", this._pasteEventListener);
+ },
+
+ willDestroyElement() {
+ this._super(...arguments);
+ this.appEvents.off("chat-composer:load-uploads", this, "_loadUploads");
+ this.composerInputEl?.removeEventListener(
+ "paste",
+ this._pasteEventListener
+ );
+ },
+
+ uploadDone(upload) {
+ this.uploads.pushObject(upload);
+ this.onUploadChanged(this.uploads);
+ },
+
+ @discourseComputed("uploads.length", "inProgressUploads.length")
+ showUploadsContainer(uploadsCount, inProgressUploadsCount) {
+ return uploadsCount > 0 || inProgressUploadsCount > 0;
+ },
+
+ @action
+ cancelUploading(upload) {
+ this.appEvents.trigger(`upload-mixin:${this.id}:cancel-upload`, {
+ fileId: upload.id,
+ });
+ this.uploads.removeObject(upload);
+ this.onUploadChanged(this.uploads);
+ },
+
+ @action
+ removeUpload(upload) {
+ this.uploads.removeObject(upload);
+ this.onUploadChanged(this.uploads);
+ },
+
+ _uploadDropTargetOptions() {
+ let targetEl;
+ if (this.chatStateManager.isFullPage) {
+ targetEl = document.querySelector(".full-page-chat");
+ } else {
+ targetEl = document.querySelector(
+ ".topic-chat-container.expanded.visible"
+ );
+ }
+
+ if (!targetEl) {
+ return this._super();
+ }
+
+ return {
+ target: targetEl,
+ };
+ },
+
+ _loadUploads(uploads) {
+ this._uppyInstance?.cancelAll();
+ this.set("uploads", uploads);
+ },
+
+ _uppyReady() {
+ if (this.siteSettings.composer_media_optimization_image_enabled) {
+ this._useUploadPlugin(UppyMediaOptimization, {
+ optimizeFn: (data, opts) =>
+ this.mediaOptimizationWorker.optimizeImage(data, opts),
+ runParallel: !this.site.isMobileDevice,
+ });
+ }
+
+ this._onPreProcessProgress((file) => {
+ const inProgressUpload = this.inProgressUploads.findBy("id", file.id);
+ if (!inProgressUpload?.processing) {
+ inProgressUpload?.set("processing", true);
+ }
+ });
+
+ this._onPreProcessComplete((file) => {
+ const inProgressUpload = this.inProgressUploads.findBy("id", file.id);
+ inProgressUpload?.set("processing", false);
+ });
+ },
+
+ @bind
+ _pasteEventListener(event) {
+ if (document.activeElement !== this.composerInputEl) {
+ return;
+ }
+
+ const { canUpload, canPasteHtml, types } = clipboardHelpers(event, {
+ siteSettings: this.siteSettings,
+ canUpload: true,
+ });
+
+ if (!canUpload || canPasteHtml || types.includes("text/plain")) {
+ return;
+ }
+
+ if (event && event.clipboardData && event.clipboardData.files) {
+ this._addFiles([...event.clipboardData.files], { pasted: true });
+ }
+ },
+});
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-composer.js b/plugins/chat/assets/javascripts/discourse/components/chat-composer.js
new file mode 100644
index 0000000000..bb9f68be19
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-composer.js
@@ -0,0 +1,732 @@
+import { isEmpty } from "@ember/utils";
+import Component from "@ember/component";
+import showModal from "discourse/lib/show-modal";
+import discourseComputed, {
+ afterRender,
+ bind,
+} from "discourse-common/utils/decorators";
+import I18n from "I18n";
+import TextareaTextManipulation from "discourse/mixins/textarea-text-manipulation";
+import userSearch from "discourse/lib/user-search";
+import { action } from "@ember/object";
+import { cancel, next, schedule, throttle } from "@ember/runloop";
+import { cloneJSON } from "discourse-common/lib/object";
+import { findRawTemplate } from "discourse-common/lib/raw-templates";
+import { emojiSearch, isSkinTonableEmoji } from "pretty-text/emoji";
+import { emojiUrlFor } from "discourse/lib/text";
+import { inject as service } from "@ember/service";
+import { readOnly, reads } from "@ember/object/computed";
+import { SKIP } from "discourse/lib/autocomplete";
+import { Promise } from "rsvp";
+import { translations } from "pretty-text/emoji/data";
+import { channelStatusName } from "discourse/plugins/chat/discourse/models/chat-channel";
+import { setupHashtagAutocomplete } from "discourse/lib/hashtag-autocomplete";
+import {
+ chatComposerButtons,
+ chatComposerButtonsDependentKeys,
+} from "discourse/plugins/chat/discourse/lib/chat-composer-buttons";
+
+const THROTTLE_MS = 150;
+
+export default Component.extend(TextareaTextManipulation, {
+ chatChannel: null,
+ lastChatChannelId: null,
+ chat: service(),
+ classNames: ["chat-composer-container"],
+ classNameBindings: ["emojiPickerVisible:with-emoji-picker"],
+ userSilenced: readOnly("details.user_silenced"),
+ chatEmojiReactionStore: service("chat-emoji-reaction-store"),
+ chatEmojiPickerManager: service("chat-emoji-picker-manager"),
+ chatStateManager: service("chat-state-manager"),
+ editingMessage: null,
+ onValueChange: null,
+ timer: null,
+ value: "",
+ inProgressUploads: null,
+ composerEventPrefix: "chat",
+ composerFocusSelector: ".chat-composer-input",
+ canAttachUploads: reads("siteSettings.chat_allow_uploads"),
+ isNetworkUnreliable: reads("chat.isNetworkUnreliable"),
+
+ @discourseComputed(...chatComposerButtonsDependentKeys())
+ inlineButtons() {
+ return chatComposerButtons(this, "inline");
+ },
+
+ @discourseComputed(...chatComposerButtonsDependentKeys())
+ dropdownButtons() {
+ return chatComposerButtons(this, "dropdown");
+ },
+
+ @discourseComputed("chatEmojiPickerManager.{opened,context}")
+ emojiPickerVisible(picker) {
+ return picker.opened && picker.context === "chat-composer";
+ },
+
+ @discourseComputed("chatStateManager.isFullPage")
+ fileUploadElementId(fullPage) {
+ return fullPage ? "chat-full-page-uploader" : "chat-widget-uploader";
+ },
+
+ init() {
+ this._super(...arguments);
+
+ this.appEvents.on("chat-composer:reply-to-set", this, "_replyToMsgChanged");
+ this.appEvents.on(
+ "upload-mixin:chat-composer-uploader:in-progress-uploads",
+ this,
+ "_inProgressUploadsChanged"
+ );
+
+ this.setProperties({
+ inProgressUploads: [],
+ _uploads: [],
+ });
+ },
+
+ didInsertElement() {
+ this._super(...arguments);
+
+ this._textarea = this.element.querySelector(".chat-composer-input");
+ this._$textarea = $(this._textarea);
+ this._applyCategoryHashtagAutocomplete(this._$textarea);
+ this._applyEmojiAutocomplete(this._$textarea);
+ this.appEvents.on("chat:focus-composer", this, "_focusTextArea");
+ this.appEvents.on("chat:insert-text", this, "insertText");
+ this._focusTextArea();
+
+ this.appEvents.on("chat:modify-selection", this, "_modifySelection");
+ this.appEvents.on(
+ "chat:open-insert-link-modal",
+ this,
+ "_openInsertLinkModal"
+ );
+ document.addEventListener("visibilitychange", this._blurInput);
+ document.addEventListener("resume", this._blurInput);
+ document.addEventListener("freeze", this._blurInput);
+
+ this.set("ready", true);
+ },
+
+ _modifySelection(opts = { type: null }) {
+ const sel = this.getSelected("", { lineVal: true });
+ if (opts.type === "bold") {
+ this.applySurround(sel, "**", "**", "bold_text");
+ } else if (opts.type === "italic") {
+ this.applySurround(sel, "_", "_", "italic_text");
+ } else if (opts.type === "code") {
+ this.applySurround(sel, "`", "`", "code_text");
+ }
+ },
+
+ _openInsertLinkModal() {
+ const selected = this.getSelected("", { lineVal: true });
+ const linkText = selected?.value;
+ showModal("insert-hyperlink").setProperties({
+ linkText,
+ toolbarEvent: {
+ addText: (text) => this.addText(selected, text),
+ },
+ });
+ },
+
+ willDestroyElement() {
+ this._super(...arguments);
+
+ this.appEvents.off(
+ "chat-composer:reply-to-set",
+ this,
+ "_replyToMsgChanged"
+ );
+ this.appEvents.off(
+ "upload-mixin:chat-composer-uploader:in-progress-uploads",
+ this,
+ "_inProgressUploadsChanged"
+ );
+
+ if (this.timer) {
+ cancel(this.timer);
+ this.timer = null;
+ }
+
+ this.appEvents.off("chat:focus-composer", this, "_focusTextArea");
+ this.appEvents.off("chat:insert-text", this, "insertText");
+ this.appEvents.off("chat:modify-selection", this, "_modifySelection");
+ this.appEvents.off(
+ "chat:open-insert-link-modal",
+ this,
+ "_openInsertLinkModal"
+ );
+ document.removeEventListener("visibilitychange", this._blurInput);
+ document.removeEventListener("resume", this._blurInput);
+ document.removeEventListener("freeze", this._blurInput);
+ },
+
+ // It is important that this is keyDown and not keyUp, otherwise
+ // we add new lines to chat message on send and on edit, because
+ // you cannot prevent default with a keyUp event -- it is like trying
+ // to shut the gate after the horse has already bolted!
+ keyDown(event) {
+ if (this.site.mobileView || event.altKey || event.metaKey) {
+ return;
+ }
+
+ // keyCode for 'Enter'
+ if (event.keyCode === 13) {
+ if (event.shiftKey) {
+ // Shift+Enter: insert newline
+ return;
+ }
+
+ // Ctrl+Enter, plain Enter: send
+ if (!event.ctrlKey) {
+ // if we are inside a code block just insert newline
+ const { pre } = this.getSelected(null, { lineVal: true });
+ if (this.isInside(pre, /(^|\n)```/g)) {
+ return;
+ }
+ }
+
+ this.sendClicked();
+ return false;
+ }
+
+ if (
+ event.key === "ArrowUp" &&
+ this._messageIsEmpty() &&
+ !this.editingMessage
+ ) {
+ event.preventDefault();
+ this.onEditLastMessageRequested();
+ }
+
+ if (event.keyCode === 27) {
+ // keyCode for 'Escape'
+ if (this.replyToMsg) {
+ this.set("value", "");
+ this._replyToMsgChanged(null);
+ return false;
+ } else if (this.editingMessage) {
+ this.set("value", "");
+ this.cancelEditing();
+ return false;
+ } else {
+ this._textarea.blur();
+ }
+ }
+ },
+
+ didReceiveAttrs() {
+ this._super(...arguments);
+
+ if (
+ !this.editingMessage &&
+ this.draft &&
+ this.chatChannel?.canModifyMessages(this.currentUser)
+ ) {
+ // uses uploads from draft here...
+ this.setProperties({
+ value: this.draft.value,
+ replyToMsg: this.draft.replyToMsg,
+ });
+
+ this._syncUploads(this.draft.uploads);
+ this.setInReplyToMsg(this.draft.replyToMsg);
+ }
+
+ if (this.editingMessage && !this.loading) {
+ this.setProperties({
+ replyToMsg: null,
+ value: this.editingMessage.message,
+ });
+
+ this._syncUploads(this.editingMessage.uploads);
+ this._focusTextArea({ ensureAtEnd: true, resizeTextarea: false });
+ }
+
+ this.set("lastChatChannelId", this.chatChannel.id);
+ this.resizeTextarea();
+ },
+
+ // the chat-composer needs to be able to set the internal list of uploads
+ // for chat-composer-uploads to preload in existing uploads for drafts
+ // and for when messages are being edited.
+ //
+ // the opposite is true as well -- when an upload is completed the chat-composer
+ // needs its internal state updated so drafts can be saved, which is handled
+ // by the uploadsChanged action
+ _syncUploads(newUploads = []) {
+ const currentUploadIds = this._uploads.mapBy("id");
+ const newUploadIds = newUploads.mapBy("id");
+
+ // don't need to load the uploads into chat-composer-uploads if
+ // nothing has changed otherwise we would rerender for no reason
+ if (
+ currentUploadIds.length === newUploadIds.length &&
+ newUploadIds.every((newUploadId) =>
+ currentUploadIds.includes(newUploadId)
+ )
+ ) {
+ return;
+ }
+
+ this.set("_uploads", cloneJSON(newUploads));
+ this.appEvents.trigger("chat-composer:load-uploads", this._uploads);
+ },
+
+ _inProgressUploadsChanged(inProgressUploads) {
+ next(() => {
+ if (this.isDestroying || this.isDestroyed) {
+ return;
+ }
+
+ this.set("inProgressUploads", inProgressUploads);
+ });
+ },
+
+ _replyToMsgChanged(replyToMsg) {
+ this.set("replyToMsg", replyToMsg);
+ this.onValueChange?.(this.value, this._uploads, replyToMsg);
+ },
+
+ @action
+ onTextareaInput(value) {
+ this.set("value", value);
+ this.resizeTextarea();
+
+ // throttle, not debounce, because we do eventually want to react during the typing
+ this.timer = throttle(this, this._handleTextareaInput, THROTTLE_MS);
+ },
+
+ @bind
+ _handleTextareaInput() {
+ this._applyUserAutocomplete();
+ this.onValueChange?.(this.value, this._uploads, this.replyToMsg);
+ },
+
+ @bind
+ _blurInput() {
+ document.activeElement?.blur();
+ },
+
+ @action
+ uploadClicked() {
+ this.element.querySelector(`#${this.fileUploadElementId}`).click();
+ },
+
+ @bind
+ didSelectEmoji(emoji) {
+ const code = `:${emoji}:`;
+ this.chatEmojiReactionStore.track(code);
+ this.addText(this.getSelected(), code);
+ },
+
+ @action
+ insertDiscourseLocalDate() {
+ showModal("discourse-local-dates-create-modal").setProperties({
+ insertDate: (markup) => {
+ this.addText(this.getSelected(), markup);
+ },
+ });
+ },
+
+ // text-area-manipulation mixin override
+ addText() {
+ this._super(...arguments);
+
+ this.resizeTextarea();
+ },
+
+ _applyUserAutocomplete() {
+ if (this.siteSettings.enable_mentions) {
+ $(this._textarea).autocomplete({
+ template: findRawTemplate("user-selector-autocomplete"),
+ key: "@",
+ width: "100%",
+ treatAsTextarea: true,
+ autoSelectFirstSuggestion: true,
+ transformComplete: (v) => v.username || v.name,
+ dataSource: (term) => userSearch({ term, includeGroups: true }),
+ afterComplete: (text) => {
+ this.set("value", text);
+ this._focusTextArea();
+ },
+ });
+ }
+ },
+
+ _applyCategoryHashtagAutocomplete($textarea) {
+ setupHashtagAutocomplete(
+ "chat-composer",
+ $textarea,
+ this.siteSettings,
+ (value) => {
+ this.set("value", value);
+ return this._focusTextArea();
+ }
+ );
+ },
+
+ _applyEmojiAutocomplete($textarea) {
+ if (!this.siteSettings.enable_emoji) {
+ return;
+ }
+
+ $textarea.autocomplete({
+ template: findRawTemplate("emoji-selector-autocomplete"),
+ key: ":",
+ afterComplete: (text) => {
+ this.set("value", text);
+ this._focusTextArea();
+ },
+ treatAsTextarea: true,
+
+ onKeyUp: (text, cp) => {
+ const matches =
+ /(?:^|[\s.\?,@\/#!%&*;:\[\]{}=\-_()])(:(?!:).?[\w-]*:?(?!:)(?:t\d?)?:?) ?$/gi.exec(
+ text.substring(0, cp)
+ );
+
+ if (matches && matches[1]) {
+ return [matches[1]];
+ }
+ },
+
+ transformComplete: (v) => {
+ if (v.code) {
+ this.chatEmojiReactionStore.track(v.code);
+ return `${v.code}:`;
+ } else {
+ $textarea.autocomplete({ cancel: true });
+ this.set("emojiPickerIsActive", true);
+ return "";
+ }
+ },
+
+ dataSource: (term) => {
+ return new Promise((resolve) => {
+ const full = `:${term}`;
+ term = term.toLowerCase();
+
+ // We need to avoid quick emoji autocomplete cause it can interfere with quick
+ // typing, set minimal length to 2
+ let minLength = Math.max(
+ this.siteSettings.emoji_autocomplete_min_chars,
+ 2
+ );
+
+ if (term.length < minLength) {
+ return resolve(SKIP);
+ }
+
+ // bypass :-p and other common typed smileys
+ if (
+ !term.match(
+ /[^-\{\}\[\]\(\)\*_\<\>\\\/].*[^-\{\}\[\]\(\)\*_\<\>\\\/]/
+ )
+ ) {
+ return resolve(SKIP);
+ }
+
+ if (term === "") {
+ if (this.chatEmojiReactionStore.favorites.length) {
+ return resolve(this.chatEmojiReactionStore.favorites.slice(0, 5));
+ } else {
+ return resolve([
+ "slight_smile",
+ "smile",
+ "wink",
+ "sunny",
+ "blush",
+ ]);
+ }
+ }
+
+ // note this will only work for emojis starting with :
+ // eg: :-)
+ const emojiTranslation =
+ this.get("site.custom_emoji_translation") || {};
+ const allTranslations = Object.assign(
+ {},
+ translations,
+ emojiTranslation
+ );
+ if (allTranslations[full]) {
+ return resolve([allTranslations[full]]);
+ }
+
+ const match = term.match(/^:?(.*?):t([2-6])?$/);
+ if (match) {
+ const name = match[1];
+ const scale = match[2];
+
+ if (isSkinTonableEmoji(name)) {
+ if (scale) {
+ return resolve([`${name}:t${scale}`]);
+ } else {
+ return resolve([2, 3, 4, 5, 6].map((x) => `${name}:t${x}`));
+ }
+ }
+ }
+
+ const options = emojiSearch(term, {
+ maxResults: 5,
+ diversity: this.chatEmojiReactionStore.diversity,
+ });
+
+ return resolve(options);
+ })
+ .then((list) => {
+ if (list === SKIP) {
+ return;
+ }
+ return list.map((code) => ({ code, src: emojiUrlFor(code) }));
+ })
+ .then((list) => {
+ if (list?.length) {
+ list.push({ label: I18n.t("composer.more_emoji"), term });
+ }
+ return list;
+ });
+ },
+ });
+ },
+
+ @afterRender
+ _focusTextArea(opts = { ensureAtEnd: false, resizeTextarea: true }) {
+ if (this.chatChannel.isDraft) {
+ return;
+ }
+
+ if (!this._textarea) {
+ return;
+ }
+
+ if (opts.resizeTextarea) {
+ this.resizeTextarea();
+ }
+
+ if (opts.ensureAtEnd) {
+ this._textarea.setSelectionRange(this.value.length, this.value.length);
+ }
+
+ if (this.capabilities.isIpadOS || this.site.mobileView) {
+ return;
+ }
+
+ schedule("afterRender", () => {
+ this._textarea?.focus();
+ });
+ },
+
+ @action
+ onEmojiSelected(code) {
+ this.emojiSelected(code);
+ this.set("emojiPickerIsActive", false);
+ },
+
+ @discourseComputed(
+ "chatChannel.{id,chatable.users.[]}",
+ "canInteractWithChat"
+ )
+ disableComposer(channel, canInteractWithChat) {
+ return (
+ (channel.isDraft && isEmpty(channel?.chatable?.users)) ||
+ !canInteractWithChat ||
+ !channel.canModifyMessages(this.currentUser)
+ );
+ },
+
+ @discourseComputed("userSilenced", "chatChannel.{chatable.users.[],id}")
+ placeholder(userSilenced, chatChannel) {
+ if (!chatChannel.canModifyMessages(this.currentUser)) {
+ return I18n.t("chat.placeholder_new_message_disallowed", {
+ status: channelStatusName(chatChannel.status).toLowerCase(),
+ });
+ }
+
+ if (chatChannel.isDraft) {
+ return I18n.t("chat.placeholder_start_conversation", {
+ usernames: chatChannel?.chatable?.users?.length
+ ? chatChannel.chatable.users.mapBy("username").join(", ")
+ : "...",
+ });
+ }
+
+ if (userSilenced) {
+ return I18n.t("chat.placeholder_silenced");
+ } else {
+ return this.messageRecipient(chatChannel);
+ }
+ },
+
+ messageRecipient(chatChannel) {
+ if (chatChannel.isDirectMessageChannel) {
+ const directMessageRecipients = chatChannel.chatable.users;
+ if (
+ directMessageRecipients.length === 1 &&
+ directMessageRecipients[0].id === this.currentUser.id
+ ) {
+ return I18n.t("chat.placeholder_self");
+ }
+
+ return I18n.t("chat.placeholder_others", {
+ messageRecipient: directMessageRecipients
+ .map((u) => u.name || `@${u.username}`)
+ .join(", "),
+ });
+ } else {
+ return I18n.t("chat.placeholder_others", {
+ messageRecipient: `#${chatChannel.title}`,
+ });
+ }
+ },
+
+ @discourseComputed(
+ "value",
+ "loading",
+ "disableComposer",
+ "inProgressUploads.[]"
+ )
+ sendDisabled(value, loading, disableComposer, inProgressUploads) {
+ if (loading || disableComposer || inProgressUploads.length > 0) {
+ return true;
+ }
+
+ return !this._messageIsValid();
+ },
+
+ @action
+ sendClicked() {
+ if (this.site.mobileView) {
+ // prevents android to hide the keyboard after sending a message
+ // we do a focusTextarea later but it's too late for android
+ document.querySelector(this.composerFocusSelector).focus();
+ }
+
+ if (this.sendDisabled) {
+ return;
+ }
+
+ this.editingMessage
+ ? this.internalEditMessage()
+ : this.internalSendMessage();
+ },
+
+ @action
+ internalSendMessage() {
+ return this.sendMessage(this.value, this._uploads).then(this.reset);
+ },
+
+ @action
+ internalEditMessage() {
+ return this.editMessage(
+ this.editingMessage,
+ this.value,
+ this._uploads
+ ).then(this.reset);
+ },
+
+ _messageIsValid() {
+ const validLength =
+ (this.value || "").trim().length >=
+ (this.siteSettings.chat_minimum_message_length || 0);
+
+ if (this.canAttachUploads) {
+ if (this._messageIsEmpty()) {
+ // If message is empty, an an upload must present for sending to be enabled
+ return this._uploads.length;
+ } else {
+ // Message is non-empty. Make sure it's long enough to be valid.
+ return validLength;
+ }
+ }
+
+ // Attachments are disabled so for a message to be valid it must be long enough.
+ return validLength;
+ },
+
+ _messageIsEmpty() {
+ return (this.value || "").trim() === "";
+ },
+
+ @action
+ reset() {
+ if (this.isDestroyed || this.isDestroying) {
+ return;
+ }
+
+ this.setProperties({
+ value: "",
+ inReplyMsg: null,
+ });
+ this._syncUploads([]);
+ this._focusTextArea({ ensureAtEnd: true, resizeTextarea: true });
+ this.onValueChange?.(this.value, this._uploads, this.replyToMsg);
+ },
+
+ @action
+ cancelReplyTo() {
+ this.set("replyToMsg", null);
+ this.setInReplyToMsg(null);
+ this.onValueChange?.(this.value, this._uploads, this.replyToMsg);
+ },
+
+ @action
+ cancelEditing() {
+ this.onCancelEditing();
+ this._focusTextArea({ ensureAtEnd: true, resizeTextarea: true });
+ },
+
+ _cursorIsOnEmptyLine() {
+ const selectionStart = this._textarea.selectionStart;
+ if (selectionStart === 0) {
+ return true;
+ } else if (this._textarea.value.charAt(selectionStart - 1) === "\n") {
+ return true;
+ } else {
+ return false;
+ }
+ },
+
+ @action
+ uploadsChanged(uploads) {
+ this.set("_uploads", cloneJSON(uploads));
+ this.onValueChange?.(this.value, this._uploads, this.replyToMsg);
+ },
+
+ @action
+ onTextareaFocusIn(target) {
+ if (!this.capabilities.isIOS) {
+ return;
+ }
+
+ // hack to prevent the whole viewport
+ // to move on focus input
+ target = document.querySelector(".chat-composer-input");
+ target.style.transform = "translateY(-99999px)";
+ target.focus();
+ window.requestAnimationFrame(() => {
+ window.requestAnimationFrame(() => {
+ target.style.transform = "";
+ });
+ });
+ },
+
+ @action
+ resizeTextarea() {
+ schedule("afterRender", () => {
+ if (!this._textarea) {
+ return;
+ }
+
+ // this is a quirk which forces us to `auto` first or textarea
+ // won't resize
+ this._textarea.style.height = "auto";
+
+ // +1 is to workaround a rounding error visible on electron
+ // causing scrollbars to show when they shouldn’t
+ this._textarea.style.height = this._textarea.scrollHeight + 1 + "px";
+ });
+ },
+});
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-draft-channel-screen.js b/plugins/chat/assets/javascripts/discourse/components/chat-draft-channel-screen.js
new file mode 100644
index 0000000000..e374564c35
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-draft-channel-screen.js
@@ -0,0 +1,53 @@
+import ChatChannel from "discourse/plugins/chat/discourse/models/chat-channel";
+import { inject as service } from "@ember/service";
+import Component from "@ember/component";
+import { action } from "@ember/object";
+import { cloneJSON } from "discourse-common/lib/object";
+export default class ChatDraftChannelScreen extends Component {
+ @service chat;
+ @service router;
+ tagName = "";
+ onSwitchChannel = null;
+
+ @action
+ onCancelChatDraft() {
+ return this.router.transitionTo("chat.index");
+ }
+
+ @action
+ onChangeSelectedUsers(users) {
+ this._fetchPreviewedChannel(users);
+ }
+
+ @action
+ onSwitchFromDraftChannel(channel) {
+ channel.set("isDraft", false);
+ this.onSwitchChannel?.(channel);
+ }
+
+ _fetchPreviewedChannel(users) {
+ this.set("previewedChannel", null);
+
+ return this.chat
+ .getDmChannelForUsernames(users.mapBy("username"))
+ .then((response) => {
+ this.set(
+ "previewedChannel",
+ ChatChannel.create(
+ Object.assign({}, response.chat_channel, { isDraft: true })
+ )
+ );
+ })
+ .catch((error) => {
+ if (error?.jqXHR?.status === 404) {
+ this.set(
+ "previewedChannel",
+ ChatChannel.create({
+ chatable: { users: cloneJSON(users) },
+ isDraft: true,
+ })
+ );
+ }
+ });
+ }
+}
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-emoji-picker.js b/plugins/chat/assets/javascripts/discourse/components/chat-emoji-picker.js
new file mode 100644
index 0000000000..4395e95d44
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-emoji-picker.js
@@ -0,0 +1,368 @@
+import Component from "@ember/component";
+import { htmlSafe } from "@ember/template";
+import { action } from "@ember/object";
+import { inject as service } from "@ember/service";
+import { tracked } from "@glimmer/tracking";
+import { emojiUrlFor } from "discourse/lib/text";
+import discourseDebounce from "discourse-common/lib/debounce";
+import { INPUT_DELAY } from "discourse-common/config/environment";
+import { bind } from "discourse-common/utils/decorators";
+import { later, schedule } from "@ember/runloop";
+
+export const FITZPATRICK_MODIFIERS = [
+ {
+ scale: 1,
+ modifier: null,
+ },
+ {
+ scale: 2,
+ modifier: ":t2",
+ },
+ {
+ scale: 3,
+ modifier: ":t3",
+ },
+ {
+ scale: 4,
+ modifier: ":t4",
+ },
+ {
+ scale: 5,
+ modifier: ":t5",
+ },
+ {
+ scale: 6,
+ modifier: ":t6",
+ },
+];
+
+export default class ChatEmojiPicker extends Component {
+ @service chatEmojiPickerManager;
+ @service emojiPickerScrollObserver;
+ @service chatEmojiReactionStore;
+ @tracked filteredEmojis = null;
+ @tracked isExpandedFitzpatrickScale = false;
+ tagName = "";
+
+ fitzpatrickModifiers = FITZPATRICK_MODIFIERS;
+
+ get groups() {
+ const emojis = this.chatEmojiPickerManager.emojis;
+ const favorites = {
+ favorites: this.chatEmojiReactionStore.favorites.map((name) => {
+ return {
+ name,
+ group: "favorites",
+ url: emojiUrlFor(name),
+ };
+ }),
+ };
+
+ return {
+ ...favorites,
+ ...emojis,
+ };
+ }
+
+ get flatEmojis() {
+ // eslint-disable-next-line no-unused-vars
+ let { favorites, ...rest } = this.chatEmojiPickerManager.emojis;
+ return Object.values(rest).flat();
+ }
+
+ get navIndicatorStyle() {
+ const section = this.chatEmojiPickerManager.lastVisibleSection;
+ const index = Object.keys(this.groups).indexOf(section);
+
+ return htmlSafe(
+ `width: ${
+ 100 / Object.keys(this.groups).length
+ }%; transform: translateX(${index * 100}%);`
+ );
+ }
+
+ get navBtnStyle() {
+ return htmlSafe(`width: ${100 / Object.keys(this.groups).length}%;`);
+ }
+
+ @action
+ didPressEscape(event) {
+ if (event.key === "Escape") {
+ this.chatEmojiPickerManager.close();
+ }
+ }
+
+ @action
+ didNavigateFitzpatrickScale(event) {
+ if (event.type !== "keyup") {
+ return;
+ }
+
+ const scaleNodes =
+ event.target
+ .closest(".chat-emoji-picker__fitzpatrick-scale")
+ ?.querySelectorAll(".chat-emoji-picker__fitzpatrick-modifier-btn") ||
+ [];
+
+ const scales = [...scaleNodes];
+
+ if (event.key === "ArrowRight") {
+ event.preventDefault();
+
+ if (event.target === scales[scales.length - 1]) {
+ scales[0].focus();
+ } else {
+ event.target.nextElementSibling?.focus();
+ }
+ }
+
+ if (event.key === "ArrowLeft") {
+ event.preventDefault();
+
+ if (event.target === scales[0]) {
+ scales[scales.length - 1].focus();
+ } else {
+ event.target.previousElementSibling?.focus();
+ }
+ }
+ }
+
+ @action
+ didToggleFitzpatrickScale(event) {
+ if (event.type === "keyup") {
+ if (event.key === "Escape") {
+ event.preventDefault();
+ this.isExpandedFitzpatrickScale = false;
+ return;
+ }
+
+ if (event.key !== "Enter") {
+ return;
+ }
+ }
+
+ this.toggleProperty("isExpandedFitzpatrickScale");
+ }
+
+ @action
+ didRequestFitzpatrickScale(scale, event) {
+ if (event.type === "keyup") {
+ if (event.key === "Escape") {
+ event.preventDefault();
+ event.stopPropagation();
+ this.isExpandedFitzpatrickScale = false;
+ this._focusCurrentFitzpatrickScale();
+ return;
+ }
+
+ if (event.key !== "Enter") {
+ return;
+ }
+ }
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ this.isExpandedFitzpatrickScale = false;
+ this.chatEmojiReactionStore.diversity = scale;
+ this._focusCurrentFitzpatrickScale();
+ }
+
+ _focusCurrentFitzpatrickScale() {
+ schedule("afterRender", () => {
+ document
+ .querySelector(".chat-emoji-picker__fitzpatrick-modifier-btn.current")
+ ?.focus();
+ });
+ }
+
+ @action
+ didInputFilter(event) {
+ if (!event.target.value.length) {
+ this.filteredEmojis = null;
+ return;
+ }
+
+ discourseDebounce(
+ this,
+ this.debouncedDidInputFilter,
+ event.target.value,
+ INPUT_DELAY
+ );
+ }
+
+ @action
+ focusFilter(target) {
+ target.focus();
+ }
+
+ debouncedDidInputFilter(filter = "") {
+ filter = filter.toLowerCase();
+
+ this.filteredEmojis = this.flatEmojis.filter(
+ (emoji) =>
+ emoji.name.toLowerCase().includes(filter) ||
+ emoji.search_aliases?.any((alias) =>
+ alias.toLowerCase().includes(filter)
+ )
+ );
+
+ schedule("afterRender", () => {
+ const scrollableContent = document.querySelector(
+ ".chat-emoji-picker__scrollable-content"
+ );
+
+ if (scrollableContent) {
+ scrollableContent.scrollTop = 0;
+ }
+ });
+ }
+
+ @action
+ didNavigateSection(event) {
+ if (event.type !== "keyup") {
+ return;
+ }
+
+ const sectionEmojis = [
+ ...event.target
+ .closest(".chat-emoji-picker__section")
+ .querySelectorAll(".emoji"),
+ ];
+
+ if (event.key === "ArrowRight") {
+ event.preventDefault();
+
+ if (event.target === sectionEmojis[sectionEmojis.length - 1]) {
+ sectionEmojis[0].focus();
+ } else {
+ event.target.nextElementSibling?.focus();
+ }
+ }
+
+ if (event.key === "ArrowLeft") {
+ event.preventDefault();
+
+ if (event.target === sectionEmojis[0]) {
+ sectionEmojis[sectionEmojis.length - 1].focus();
+ } else {
+ event.target.previousElementSibling?.focus();
+ }
+ }
+
+ if (event.key === "ArrowDown") {
+ event.preventDefault();
+
+ sectionEmojis
+ .filter((c) => c.offsetTop > event.target.offsetTop)
+ .find((c) => c.offsetLeft === event.target.offsetLeft)
+ ?.focus();
+ }
+
+ if (event.key === "ArrowUp") {
+ event.preventDefault();
+
+ sectionEmojis
+ .reverse()
+ .filter((c) => c.offsetTop < event.target.offsetTop)
+ .find((c) => c.offsetLeft === event.target.offsetLeft)
+ ?.focus();
+ }
+ }
+
+ @action
+ didSelectEmoji(event) {
+ if (!event.target.classList.contains("emoji")) {
+ return;
+ }
+
+ if (
+ event.type === "click" ||
+ (event.type === "keyup" && event.key === "Enter")
+ ) {
+ event.preventDefault();
+ event.stopPropagation();
+ const originalTarget = event.target;
+ let emoji = event.target.dataset.emoji;
+ const tonable = event.target.dataset.tonable;
+ const diversity = this.chatEmojiReactionStore.diversity;
+ if (tonable && diversity > 1) {
+ emoji = `${emoji}:t${diversity}`;
+ }
+
+ this.chatEmojiPickerManager.didSelectEmoji(emoji);
+
+ schedule("afterRender", () => {
+ originalTarget.focus();
+ });
+ }
+ }
+
+ @action
+ didFocusFirstEmoji(event) {
+ event.preventDefault();
+ const section = event.target.closest(".chat-emoji-picker__section").dataset
+ .section;
+ this.didRequestSection(section);
+ }
+
+ @action
+ didRequestSection(section) {
+ const scrollableContent = document.querySelector(
+ ".chat-emoji-picker__scrollable-content"
+ );
+
+ this.filteredEmojis = null;
+
+ // we disable scroll listener during requesting section
+ // to avoid it from detecting another section during scroll to requested section
+ this.emojiPickerScrollObserver.enabled = false;
+ this.chatEmojiPickerManager.addVisibleSections([section]);
+ this.chatEmojiPickerManager.lastVisibleSection = section;
+
+ // iOS hack to avoid blank div when requesting section during momentum
+ if (scrollableContent && this.capabilities.isIOS) {
+ document.querySelector(
+ ".chat-emoji-picker__scrollable-content"
+ ).style.overflow = "hidden";
+ }
+
+ schedule("afterRender", () => {
+ document
+ .querySelector(`.chat-emoji-picker__section[data-section="${section}"]`)
+ .scrollIntoView({
+ behavior: "auto",
+ block: "start",
+ inline: "nearest",
+ });
+
+ later(() => {
+ // iOS hack to avoid blank div when requesting section during momentum
+ if (scrollableContent && this.capabilities.isIOS) {
+ document.querySelector(
+ ".chat-emoji-picker__scrollable-content"
+ ).style.overflow = "scroll";
+ }
+
+ this.emojiPickerScrollObserver.enabled = true;
+ }, 200);
+ });
+ }
+
+ @action
+ addClickOutsideEventListener() {
+ document.addEventListener("click", this.didClickOutside);
+ }
+
+ @action
+ removeClickOutsideEventListener() {
+ document.removeEventListener("click", this.didClickOutside);
+ }
+
+ @bind
+ didClickOutside(event) {
+ if (!event.target.closest(".chat-emoji-picker")) {
+ this.chatEmojiPickerManager.close();
+ }
+ }
+}
diff --git a/plugins/chat/assets/javascripts/discourse/components/chat-live-pane.js b/plugins/chat/assets/javascripts/discourse/components/chat-live-pane.js
new file mode 100644
index 0000000000..c23974fa4e
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/components/chat-live-pane.js
@@ -0,0 +1,1493 @@
+import isElementInViewport from "discourse/lib/is-element-in-viewport";
+import ChatApi from "discourse/plugins/chat/discourse/lib/chat-api";
+import { cloneJSON } from "discourse-common/lib/object";
+import ChatChannel from "discourse/plugins/chat/discourse/models/chat-channel";
+import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message";
+import Component from "@ember/component";
+import discourseComputed, {
+ afterRender,
+ bind,
+ observes,
+} from "discourse-common/utils/decorators";
+import discourseDebounce from "discourse-common/lib/debounce";
+import EmberObject, { action } from "@ember/object";
+import I18n from "I18n";
+import { A } from "@ember/array";
+import { ajax } from "discourse/lib/ajax";
+import { popupAjaxError } from "discourse/lib/ajax-error";
+import { cancel, next, schedule, throttle } from "@ember/runloop";
+import discourseLater from "discourse-common/lib/later";
+import { inject as service } from "@ember/service";
+import { Promise } from "rsvp";
+import { resetIdle } from "discourse/lib/desktop-notifications";
+import { capitalize } from "@ember/string";
+import {
+ onPresenceChange,
+ removeOnPresenceChange,
+} from "discourse/lib/user-presence";
+import isZoomed from "discourse/plugins/chat/discourse/lib/zoom-check";
+import { isTesting } from "discourse-common/config/environment";
+
+const MAX_RECENT_MSGS = 100;
+const STICKY_SCROLL_LENIENCE = 50;
+const PAGE_SIZE = 50;
+
+const SCROLL_HANDLER_THROTTLE_MS = isTesting() ? 0 : 100;
+const FETCH_MORE_MESSAGES_THROTTLE_MS = isTesting() ? 0 : 500;
+
+const PAST = "past";
+const FUTURE = "future";
+
+export default Component.extend({
+ classNameBindings: [":chat-live-pane", "sendingLoading", "loading"],
+ chatChannel: null,
+ registeredChatChannelId: null, // ?Number
+ loading: false,
+ loadingMorePast: false,
+ loadingMoreFuture: false,
+ hoveredMessageId: null,
+ onSwitchChannel: null,
+
+ allPastMessagesLoaded: false,
+ sendingLoading: false,
+ selectingMessages: false,
+ stickyScroll: true,
+ stickyScrollTimer: null,
+ showChatQuoteSuccess: false,
+ showCloseFullScreenBtn: false,
+ includeHeader: true,
+
+ editingMessage: null, // ?Message
+ replyToMsg: null, // ?Message
+ details: null, // Object { chat_channel_id, ... }
+ messages: null, // Array
+ messageLookup: null, // Object
+
+
+ <%- if @unsubscribe_link %>
+ <%= raw(t 'user_notifications.chat_summary.unsubscribe',
+ site_link: html_site_link,
+ email_preferences_link: link_to(t('user_notifications.chat_summary.your_chat_settings'), @preferences_path),
+ unsubscribe_link: link_to(t('user_notifications.digest.click_here'), @unsubscribe_link)) %>
+ <%- else %>
+ <%= raw(t 'user_notifications.chat_summary.unsubscribe_no_link',
+ site_link: html_site_link,
+ email_preferences_link: link_to(t('user_notifications.chat_summary.your_chat_settings'), @preferences_path)) %>
+ <%- end %>
+
+ {{i18n "chat.incoming_webhooks.title"}}
+
+ {{#if this.creatingNew}}
+
+ {{#each this.tabs as |tab|}}
+
+
+
+ {{#each this.buttons as |button|}}
+
+
+ {{else}}
+ {{d-icon "far-image"}}
+ {{/if}}
+ {{else}}
+ {{d-icon "file-alt"}}
+ {{/if}}
+
+
+
+
+ {{d-icon "comment"}}
+ {{i18n "chat.draft_channel_screen.header"}}
+
+
+ {{/if}}
+
+ {{else}}
+
+ {{i18n
+ (concat "chat.emoji_picker." section)
+ translatedFallback=section
+ }}
+
+
+ {{/let}}
+
+ {{#if
+ (includes this.chatEmojiPickerManager.visibleSections section)
+ }}
+ {{#each emojis as |emoji index|}}
+ {{! first emoji has already been rendered, we don't want to re render or would lose focus}}
+ {{#if (gt index 0)}}
+
+ {{/if}}
+ {{/each}}
+ {{/if}}
+
+ {{#each this.secondaryButtons as |button|}}
+
+
+ {{#if
+ (or this.messageCapabilities.canReact this.messageCapabilities.canReply)
+ }}
+
+{{else if (eq this.type this.VIDEO_TYPE)}}
+
+{{else}}
+
+ {{@upload.original_filename}}
+
+{{/if}}
diff --git a/plugins/chat/assets/javascripts/discourse/templates/components/chat-user-avatar.hbs b/plugins/chat/assets/javascripts/discourse/templates/components/chat-user-avatar.hbs
new file mode 100644
index 0000000000..da21254b34
--- /dev/null
+++ b/plugins/chat/assets/javascripts/discourse/templates/components/chat-user-avatar.hbs
@@ -0,0 +1,9 @@
+
+ {{#each this.users as |user|}}
+
+ /hooks/:key. يتألف الحمل من معلمة نصية فردية، وهي مقيَّدة إلى 2000 حرف.
نحن ندعم أيضًا المعلمات النصية بتنسيق Slack، مع استخراج الروابط والإشارات بناءً على التنسيق في https://api.slack.com/reference/surfaces/formatting، لكن يجب استخدام نقطة النهاية /hooks/:key/slack من أجل ذلك."
+ selection:
+ cancel: "إلغاء"
+ quote_selection: "اقتباس في الموضوع"
+ copy: "نسخ"
+ move_selection_to_channel: "النقل إلى القناة"
+ error: "حدث خطأ في أثناء نقل رسائل الدردشة"
+ title: "نقل الدردشة إلى الموضوع"
+ new_topic:
+ title: "النقل إلى موضوع جديد"
+ instructions:
+ zero: "أنت على وشك إنشاء موضوع جديد وتعبئته برسالتَي الدردشة (%{count}) الذين حدَّدتهما."
+ one: "أنت على وشك إنشاء موضوع جديد وتعبئته برسالة الدردشة التي حدَّدتها."
+ two: "أنت على وشك إنشاء موضوع جديد وتعبئته برسالتَي الدردشة (%{count}) الذين حدَّدتهما."
+ few: "أنت على وشك إنشاء موضوع جديد وتعبئته برسالتَي الدردشة (%{count}) الذين حدَّدتهما."
+ many: "أنت على وشك إنشاء موضوع جديد وتعبئته برسالتَي الدردشة (%{count}) الذين حدَّدتهما."
+ other: "أنت على وشك إنشاء موضوع جديد وتعبئته برسالتَي الدردشة (%{count}) الذين حدَّدتهما."
+ instructions_channel_archive: "أنت على وشك إنشاء موضوع جديد وأٍِرشفة رسائل القناة إليه."
+ existing_topic:
+ title: "النقل إلى موضوع حالي"
+ instructions:
+ zero: "يُرجى اختيار الموضوع الذي ترغب في نقل رسالتَي الدردشة (%{count}) إليه."
+ one: "يُرجى اختيار الموضوع الذي ترغب في نقل رسالة الدردشة إليه."
+ two: "يُرجى اختيار الموضوع الذي ترغب في نقل رسالتَي الدردشة (%{count}) إليه."
+ few: "يُرجى اختيار الموضوع الذي ترغب في نقل رسالتَي الدردشة (%{count}) إليه."
+ many: "يُرجى اختيار الموضوع الذي ترغب في نقل رسالتَي الدردشة (%{count}) إليه."
+ other: "يُرجى اختيار الموضوع الذي ترغب في نقل رسالتَي الدردشة (%{count}) إليه."
+ instructions_channel_archive: "يُرجى اختيار الموضوع الذي ترغب في أرشفة رسائل القناة إليه."
+ new_message:
+ title: "النقل إلى رسالة جديدة"
+ instructions:
+ zero: "أنت على وشك إنشاء رسالة جديدة وتعبئتها برسالتَي الدردشة (%{count}) الذين حدَّدتهما."
+ one: "أنت على وشك إنشاء رسالة جديدة وتعبئتها برسالة الدردشة التي حدَّدتها."
+ two: "أنت على وشك إنشاء رسالة جديدة وتعبئتها برسالتَي الدردشة (%{count}) الذين حدَّدتهما."
+ few: "أنت على وشك إنشاء رسالة جديدة وتعبئتها برسالتَي الدردشة (%{count}) الذين حدَّدتهما."
+ many: "أنت على وشك إنشاء رسالة جديدة وتعبئتها برسالتَي الدردشة (%{count}) الذين حدَّدتهما."
+ other: "أنت على وشك إنشاء رسالة جديدة وتعبئتها برسالتَي الدردشة (%{count}) الذين حدَّدتهما."
+ replying_indicator:
+ single_user: "%{username} يكتب"
+ multiple_users: "%{commaSeparatedUsernames} و%{lastUsername} يكتبون"
+ many_users:
+ zero: "%{commaSeparatedUsernames} و%{count} آخرون يكتبون"
+ one: "%{commaSeparatedUsernames} و%{count} آخر يكتبان"
+ two: "%{commaSeparatedUsernames} و%{count} آخران يكتبون"
+ few: "%{commaSeparatedUsernames} و%{count} آخرون يكتبون"
+ many: "%{commaSeparatedUsernames} و%{count} آخرون يكتبون"
+ other: "%{commaSeparatedUsernames} و%{count} آخرون يكتبون"
+ retention_reminders:
+ public: "يتم الاحتفاظ بسجل القناة لمدة %{days} من الأيام."
+ dm: "يتم الاحتفاظ بسجل الدردشة الشخصية لمدة %{days} من الأيام."
+ topic_button_title: "الدردشة"
+ flags:
+ off_topic: "هذه الرسالة ليست ذات صلة بالمناقشة الحالية كما هو محدَّد في عنوان القناة، وربما ينبغي نقلها إلى مكانٍ آخر."
+ inappropriate: "تحتوي هذه الرسالة على محتوى قد يعتبره الشخص العاقل مسيئًا أو مهينًا أو ينتهك إرشادات المجتمع لدينا."
+ spam: "هذه الرسالة إعلانية أو تخريبية. إنها ليست مفيدة أو ذات صلة بالقناة الحالية."
+ notify_user: "أريد التحدث إلى هذا الشخص مباشرةً وشخصيًا بشأن رسالته."
+ notify_moderators: "تتطلب هذه الرسالة انتباه فريق العمل لسبب آخر غير مُدرَج أعلاه."
+ flagging:
+ action: "الإبلاغ عن الرسالة"
+ emoji_picker:
+ favorites: "المستخدمة بشكلٍ متكرر"
+ smileys_&_emotion: "الرموز التعبيرية"
+ objects: "الأشياء"
+ people_&_body: "الأشخاص والجسم"
+ travel_&_places: "السفر والأماكن"
+ animals_&_nature: "الحيوانات والطبيعة"
+ food_&_drink: "الطعام والشراب"
+ activities: "الأنشطة"
+ flags: "البلاغات"
+ symbols: "الرموز"
+ search_placeholder: "البحث باسم الرمز التعبيري والاسم المستعار..."
+ no_results: "لا توجد نتائج"
+ draft_channel_screen:
+ header: "رسالة جديدة"
+ cancel: "إلغاء"
+ notifications:
+ chat_invitation: "دعاك للانضمام إلى قناة دردشة"
+ chat_invitation_html: "دعاك %{username} للانضمام إلى قناة دردشة"
+ chat_quoted: "%{username} %{description}"
+ popup:
+ chat_mention:
+ direct: 'أشار إليك في "%{channel}"'
+ direct_html: 'أشار %{username} إليك في "%{channel}"'
+ other_plain: 'أشار إلى %{identifier} في "%{channel}"'
+ other_html: 'أشار %{username} إلى %{identifier} في "%{channel}"'
+ direct_message_chat_mention:
+ direct: "أشار إليك في دردشة شخصية"
+ direct_html: "أشار %{username} إليك في دردشة شخصية"
+ other_plain: "أشار إلى %{identifier} في دردشة شخصية"
+ other_html: "أشار %{username} إلى %{identifier} في دردشة شخصية"
+ chat_message: "رسالة دردشة جديدة"
+ chat_quoted: "اقتبس %{username} رسالة الدردشة الخاصة بك"
+ titles:
+ chat_mention: "إشارة في الدردشة"
+ chat_invitation: "دعوة الدردشة"
+ chat_quoted: "تم اقتباس الدردشة"
+ action_codes:
+ chat:
+ enabled: 'فعَّل %{who} في %{when}'
+ disabled: "أغلق %{who} الدردشة في %{when}"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ title: إرسال رسالة دردشة
+ fields:
+ chat_channel_id:
+ label: معرِّف قناة الدردشة
+ message:
+ label: رسالة
+ sender:
+ label: المرسل
+ description: يتم ضبطه افتراضيًا على النظام
+ review:
+ transcript:
+ view: "عرض نص الرسائل السابقة"
+ types:
+ reviewable_chat_message:
+ title: "رسالة دردشة تم الإبلاغ عنها"
+ flagged_by: "تم الإبلاغ بواسطة"
+ keyboard_shortcuts_help:
+ chat:
+ title: "الدردشة"
+ keyboard_shortcuts:
+ switch_channel_arrows: "%{shortcut} تبديل القناة"
+ open_quick_channel_selector: "%{shortcut} فتح محدِّد قناة سريع"
+ open_insert_link_modal: "%{shortcut} إدراج رابط تشعبي (أداة الإنشاء فقط)"
+ composer_bold: "%{shortcut} غامق (أداة الإنشاء فقط)"
+ composer_italic: "%{shortcut} مائل (أداة الإنشاء فقط)"
+ composer_code: "%{shortcut} رمز برمجي (أداة الإنشاء فقط)"
+ drawer_open: "%{shortcut} فتح درج الدردشة"
+ drawer_close: "%{shortcut} إغلاق درج الدردشة"
+ topic_statuses:
+ chat:
+ help: "الدردشة مفعَّلة لهذا الموضوع"
+ user:
+ allow_private_messages: "السماح للمستخدمين الآخرين بإرسال رسائل شخصية إليَّ ورسائل مباشرة في الدردشة"
+ muted_users_instructions: "منع كل الإشعارات والرسائل الشخصية والرسائل المباشرة في الدردشة من هؤلاء المستخدمين."
+ allowed_pm_users_instructions: "اسمح فقط بالرسائل الشخصية أو الرسائل المباشرة في الدردشة من هؤلاء المستخدمين."
+ allow_private_messages_from_specific_users: "السماح للمستخدمين المحدَّدين فقط بإرسال رسائل شخصية إليَّ أو رسائل مباشرة في الدردشة"
+ ignored_users_instructions: "منع كل المنشورات والرسائل والإشعارات والرسائل الشخصية والرسائل المباشرة في الدردشة من هؤلاء المستخدمين."
+ user_menu:
+ no_chat_notifications_title: "ليس لديك أي إشعارات دردشة حتى الآن"
+ no_chat_notifications_body: >
+ سيتم إرسال إشعار إليك في هذه اللوحة عندما يراسلك أحدهم مباشرةً أو يشير إليك @mention في الدردشة. سيتم أيضًا إرسال الإشعارات إلى بريدك الإلكتروني في حال عدم تسجيلك الدخول لفترة من الوقت.
انقر على العنوان الموجود أعلى أي قناة دردشة لضبط التنبيهات التي تتلقاها في تلك القناة. للمزيد من المعلومات، راجع تفضيلات الإشعارات.
+ tabs:
+ chat_notifications: "إشعارات الدردشة"
+ chat_notifications_with_unread:
+ zero: "إشعارات الدردشة - %{count} إشعارًا غير مقروء"
+ one: "إشعارات الدردشة - إشعار واحد (%{count}) غير مقروء"
+ two: "إشعارات الدردشة - إشعاران (%{count}) غير مقروءين"
+ few: "إشعارات الدردشة - %{count} إشعارات غير مقروءة"
+ many: "إشعارات الدردشة - %{count} إشعارًا غير مقروء"
+ other: "إشعارات الدردشة - %{count} إشعارًا غير مقروء"
diff --git a/plugins/chat/config/locales/client.be.yml b/plugins/chat/config/locales/client.be.yml
new file mode 100644
index 0000000000..2ad6f3296a
--- /dev/null
+++ b/plugins/chat/config/locales/client.be.yml
@@ -0,0 +1,92 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+be:
+ js:
+ chat:
+ create: "стварыць"
+ cancel: "адмяніць"
+ channel_settings:
+ edit: "рэдагаваць"
+ add: "дадаць"
+ close: "зачыніць"
+ delete: "выдаляць"
+ muted: "ігнаруемай"
+ joined: "рэгістрацыя"
+ email_frequency:
+ never: "ніколі"
+ flag: "Пазначыць"
+ reply: "Адказаць"
+ edit: "рэдагаваць"
+ bookmark_message: "закладка"
+ save: "захаваць"
+ exit: "назад"
+ channel_status:
+ closed: "Закрыта"
+ browse:
+ back: "Назад"
+ filter_all: Усё
+ filter_closed: Закрыта
+ chat_message_separator:
+ today: сёння
+ yesterday: учора
+ about_view:
+ title: Загаловак
+ description: Апісанне
+ channel_info:
+ back_to_channel: "Назад"
+ tabs:
+ about: Аб тэме
+ members: Удзельнікі
+ settings: Налады
+ direct_message_creator:
+ title: новае паведамленне
+ prefix: "да:"
+ create_channel:
+ type: "тып"
+ types:
+ category: "катэгорыя"
+ topic: "тэма"
+ composer:
+ italic_text: "выдзялення тэксту"
+ bold_text: "Моцнае вылучэнне тэксту"
+ notification_levels:
+ never: "ніколі"
+ settings:
+ followed: "рэгістрацыя"
+ notifications: "Натыфікацыі"
+ preview: "папярэдні прагляд"
+ save: "захаваць"
+ saved: "Захавана"
+ incoming_webhooks:
+ back: "Назад"
+ description: "Апісанне"
+ delete: "выдаляць"
+ emoji: "Emoji"
+ name: "імя"
+ save: "захаваць"
+ edit: "рэдагаваць"
+ system: "сістэма"
+ url: "URL спасылка"
+ username: "Імя карыстальніка"
+ selection:
+ cancel: "адмяніць"
+ copy: "капіяваць"
+ new_topic:
+ title: "Перанос новай тэмы"
+ existing_topic:
+ title: "Перанос наяўнай тэмы"
+ emoji_picker:
+ flags: "сцягі"
+ draft_channel_screen:
+ header: "новае паведамленне"
+ cancel: "адмяніць"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ fields:
+ message:
+ label: Паказаць выдаленыя паведамленні...
diff --git a/plugins/chat/config/locales/client.bg.yml b/plugins/chat/config/locales/client.bg.yml
new file mode 100644
index 0000000000..e4c01859e3
--- /dev/null
+++ b/plugins/chat/config/locales/client.bg.yml
@@ -0,0 +1,109 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+bg:
+ js:
+ chat:
+ cancel: "Прекрати"
+ channel_settings:
+ edit: "Редактирай"
+ add: "Добави "
+ join: "Влизане"
+ leave: "Напусни"
+ close: "Затвори"
+ delete: "Изтрий"
+ muted: "заглуши"
+ joined: "присъединен"
+ email_frequency:
+ never: "Никога"
+ flag: "Сигнализиране"
+ join: "Влизане"
+ mention_warning:
+ dismiss: "отмени"
+ reply: "Отговорете"
+ edit: "Редактирай"
+ rebake_message: "Прегенерирай HTML "
+ bookmark_message: "Отметка"
+ bookmark_message_edit: "Редактиране на отметка"
+ save: "Запази "
+ sounds:
+ none: "Без"
+ exit: "назад"
+ channel_status:
+ closed: "Затворена"
+ open: "Отвори"
+ browse:
+ back: "Назад"
+ filter_all: Всички
+ filter_closed: Затворена
+ chat_message_separator:
+ today: Днес
+ yesterday: Вчера
+ about_view:
+ title: Заглавие
+ description: Описание
+ channel_info:
+ back_to_channel: "Назад"
+ tabs:
+ about: Относно
+ members: Членове
+ settings: Настройки
+ direct_message_creator:
+ title: Ново Съобщение
+ prefix: "До:"
+ create_channel:
+ type: "Тип"
+ types:
+ category: "Категория"
+ topic: "Тема"
+ composer:
+ italic_text: "Подчертан текст"
+ bold_text: "удебелен текст"
+ notification_levels:
+ never: "Никога"
+ settings:
+ follow: "Влизане"
+ followed: "Присъединен"
+ notifications: "Известия"
+ save: "Запази "
+ saved: "Запазено"
+ unfollow: "Напусни"
+ incoming_webhooks:
+ back: "Назад"
+ description: "Описание"
+ delete: "Изтрий"
+ emoji: "Емотикони"
+ name: "Име"
+ save: "Запази "
+ edit: "Редактирай"
+ system: "система"
+ url: "URL"
+ username: "Потребителско име"
+ selection:
+ cancel: "Прекрати"
+ copy: "Копирай"
+ new_topic:
+ title: "Премести в нова тема"
+ existing_topic:
+ title: "Преместете в съществуваща тема."
+ emoji_picker:
+ objects: "Обекти"
+ activities: "Дейности"
+ flags: "Сигнали"
+ symbols: "Символи"
+ draft_channel_screen:
+ header: "Ново Съобщение"
+ cancel: "Прекрати"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ fields:
+ message:
+ label: Съобщение
+ review:
+ types:
+ reviewable_chat_message:
+ flagged_by: "Означено от"
diff --git a/plugins/chat/config/locales/client.bs_BA.yml b/plugins/chat/config/locales/client.bs_BA.yml
new file mode 100644
index 0000000000..cd6aa603aa
--- /dev/null
+++ b/plugins/chat/config/locales/client.bs_BA.yml
@@ -0,0 +1,115 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+bs_BA:
+ js:
+ chat:
+ create: "napravi"
+ cancel: "Odustani"
+ channel_settings:
+ edit: "Edit"
+ add: "Add"
+ join: "Učlani se"
+ leave: "Napusti"
+ close: "Zatvori"
+ delete: "Delete"
+ edited: "promjenjeno"
+ muted: "utišani"
+ joined: "pridružio se"
+ email_frequency:
+ never: "Nikad"
+ flag: "Prijavi"
+ join: "Učlani se"
+ mention_warning:
+ dismiss: "odbaci"
+ reply: "Odgovori"
+ edit: "Edit"
+ rebake_message: "Popravi HTML"
+ bookmark_message: "Sačuvaj"
+ save: "Save"
+ sounds:
+ none: "Ništa"
+ exit: "prethodno"
+ channel_status:
+ closed: "Zatvoreno"
+ open: "Otvori"
+ browse:
+ back: "Prethodno"
+ filter_all: All
+ filter_closed: Zatvoreno
+ chat_message_separator:
+ today: Today
+ yesterday: Yesterday
+ about_view:
+ title: Naslov
+ description: Opis
+ channel_info:
+ back_to_channel: "Prethodno"
+ tabs:
+ about: O nama
+ members: Članovi
+ settings: Postavke
+ direct_message_creator:
+ title: Nova poruka
+ prefix: "Za:"
+ create_channel:
+ type: "Tip"
+ types:
+ category: "Kategorija"
+ topic: "Topic"
+ composer:
+ italic_text: "ukošen tekst"
+ bold_text: "bold tekst"
+ notification_levels:
+ never: "Nikad"
+ settings:
+ follow: "Učlani se"
+ followed: "Pridružio se"
+ notifications: "Obavijest"
+ preview: "Pregled"
+ save: "Save"
+ saved: "Spašeno"
+ unfollow: "Napusti"
+ incoming_webhooks:
+ back: "Prethodno"
+ description: "Opis"
+ delete: "Delete"
+ emoji: "Emoji"
+ name: "Ime"
+ save: "Save"
+ edit: "Edit"
+ system: "system"
+ url: "URL"
+ username: "Nadimak"
+ selection:
+ cancel: "Odustani"
+ copy: "Copy"
+ new_topic:
+ title: "Move to New Topic"
+ existing_topic:
+ title: "Move to Existing Topic"
+ new_message:
+ title: "Premjesti u novu poruku"
+ emoji_picker:
+ objects: "Objekti"
+ activities: "Aktivnosti"
+ flags: "Flags"
+ symbols: "Simboli"
+ draft_channel_screen:
+ header: "Nova poruka"
+ cancel: "Odustani"
+ notifications:
+ chat_quoted: "%{username} %{description}"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ fields:
+ message:
+ label: Privatna Poruka
+ review:
+ types:
+ reviewable_chat_message:
+ flagged_by: "Kaznio je"
diff --git a/plugins/chat/config/locales/client.ca.yml b/plugins/chat/config/locales/client.ca.yml
new file mode 100644
index 0000000000..d5e9407c27
--- /dev/null
+++ b/plugins/chat/config/locales/client.ca.yml
@@ -0,0 +1,116 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+ca:
+ js:
+ chat:
+ create: "Crea"
+ cancel: "Cancel·la"
+ channel_settings:
+ edit: "Edita"
+ add: "Afegeix"
+ join: "Registre"
+ leave: "Abandona"
+ close: "Tanca"
+ delete: "Suprimeix"
+ edited: "editat"
+ muted: "silenciat"
+ joined: "registrat"
+ email_frequency:
+ never: "Mai"
+ flag: "Bandera"
+ join: "Registre"
+ mention_warning:
+ dismiss: "descarta-ho"
+ reply: "Respon"
+ edit: "Edita"
+ rebake_message: "Refés HTML"
+ bookmark_message: "Preferit"
+ bookmark_message_edit: "Edita el marcador"
+ save: "Desa"
+ sounds:
+ none: "Cap"
+ exit: "enrere"
+ channel_status:
+ closed: "Tancat"
+ open: "Obre"
+ browse:
+ back: "Enrere"
+ filter_all: Tot
+ filter_closed: Tancat
+ chat_message_separator:
+ today: Avui
+ yesterday: Ahir
+ about_view:
+ title: Títol
+ description: Descripció
+ channel_info:
+ back_to_channel: "Enrere"
+ tabs:
+ about: Quant a
+ members: Membres
+ settings: Configuració
+ direct_message_creator:
+ title: Missatge nou
+ prefix: "A:"
+ create_channel:
+ type: "Tipus"
+ types:
+ category: "Categoria"
+ topic: "Tema"
+ composer:
+ italic_text: "text en cursiva"
+ bold_text: "text en negreta"
+ notification_levels:
+ never: "Mai"
+ settings:
+ follow: "Registre"
+ followed: "Registrat"
+ notifications: "Notificacions"
+ preview: "Previsualitza"
+ save: "Desa"
+ saved: "Desat"
+ unfollow: "Abandona"
+ incoming_webhooks:
+ back: "Enrere"
+ description: "Descripció"
+ delete: "Suprimeix"
+ emoji: "Emoji"
+ name: "Nom"
+ save: "Desa"
+ edit: "Edita"
+ system: "sistema"
+ url: "URL"
+ username: "Nom d'usuari "
+ selection:
+ cancel: "Cancel·la"
+ copy: "Còpia"
+ new_topic:
+ title: "Mou a un tema nou"
+ existing_topic:
+ title: "Mou a un tema existent"
+ new_message:
+ title: "Mou a missatge nou"
+ emoji_picker:
+ objects: "Objectes"
+ activities: "Activitats"
+ flags: "Banderes"
+ symbols: "Símbols"
+ draft_channel_screen:
+ header: "Missatge nou"
+ cancel: "Cancel·la"
+ notifications:
+ chat_quoted: "%{username} %{description}"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ fields:
+ message:
+ label: Missatge
+ review:
+ types:
+ reviewable_chat_message:
+ flagged_by: "Marcat amb bandera per"
diff --git a/plugins/chat/config/locales/client.cs.yml b/plugins/chat/config/locales/client.cs.yml
new file mode 100644
index 0000000000..6948e7b2f5
--- /dev/null
+++ b/plugins/chat/config/locales/client.cs.yml
@@ -0,0 +1,111 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+cs:
+ js:
+ chat:
+ create: "Vytvoř"
+ cancel: "Zrušit"
+ channel_settings:
+ edit: "Upravit"
+ add: "přidat"
+ join: "Přidat se ke skupině"
+ leave: "Opustit skupinu"
+ close: "Zavřít"
+ delete: "Smazat"
+ muted: "ztišení"
+ joined: "účet vytvořen"
+ email_frequency:
+ never: "Nikdy"
+ flag: "Nahlášení"
+ join: "Přidat se ke skupině"
+ mention_warning:
+ dismiss: "označit jako přečtené"
+ reply: "Odpověď"
+ edit: "Upravit"
+ rebake_message: "Obnovit HTML"
+ bookmark_message: "Založit"
+ bookmark_message_edit: "Upravit záložku"
+ save: "Uložit"
+ sounds:
+ none: "Žádná"
+ exit: "zpět"
+ channel_status:
+ closed: "Uzavřeno"
+ open: "Otevřít"
+ browse:
+ back: "Zpět"
+ filter_all: Celkem
+ filter_closed: Uzavřeno
+ chat_message_separator:
+ today: Dnes
+ yesterday: Včera
+ about_view:
+ title: Nadpis
+ description: Popis
+ channel_info:
+ back_to_channel: "Zpět"
+ tabs:
+ about: O fóru
+ members: Členové
+ settings: Nastavení
+ direct_message_creator:
+ title: Nová zpráva
+ prefix: "Komu:"
+ create_channel:
+ type: "Typ"
+ types:
+ category: "Kategorie"
+ topic: "Témata"
+ composer:
+ italic_text: "text kurzívou"
+ bold_text: "tučný text"
+ notification_levels:
+ never: "Nikdy"
+ settings:
+ follow: "Přidat se ke skupině"
+ followed: "Účet vytvořen"
+ notifications: "Upozornění"
+ preview: "Náhled"
+ save: "Uložit"
+ saved: "Uloženo"
+ unfollow: "Opustit skupinu"
+ incoming_webhooks:
+ back: "Zpět"
+ description: "Popis"
+ delete: "Smazat"
+ emoji: "Smajlíky :)"
+ name: "Jméno"
+ save: "Uložit"
+ edit: "Upravit"
+ system: "systémové soukromé zprávy"
+ url: "URL"
+ username: "Uživatelské jméno"
+ selection:
+ cancel: "Zrušit"
+ copy: "Kopírovat"
+ new_topic:
+ title: "Rozdělit téma"
+ existing_topic:
+ title: "Sloučit téma"
+ emoji_picker:
+ objects: "Objekty"
+ flags: "Nahlášení"
+ draft_channel_screen:
+ header: "Nová zpráva"
+ cancel: "Zrušit"
+ notifications:
+ chat_quoted: "%{username} %{description}"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ fields:
+ message:
+ label: Zpráva
+ review:
+ types:
+ reviewable_chat_message:
+ flagged_by: "Nahlásil"
diff --git a/plugins/chat/config/locales/client.da.yml b/plugins/chat/config/locales/client.da.yml
new file mode 100644
index 0000000000..3a3edc2944
--- /dev/null
+++ b/plugins/chat/config/locales/client.da.yml
@@ -0,0 +1,232 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+da:
+ js:
+ admin:
+ logs:
+ staff_actions:
+ actions:
+ chat_channel_status_change: "Status for chat-kanal ændret"
+ chat_channel_delete: "Chat-kanal slettet"
+ api:
+ scopes:
+ descriptions:
+ chat:
+ create_message: "Opret en chatbesked i en angivet kanal."
+ chat:
+ dates:
+ time_tiny: "tt:mm"
+ all_loaded: "Viser alle beskeder"
+ already_enabled: "Chat er allerede aktiveret på dette emne. Opdater venligst."
+ disabled_for_topic: "Chat er deaktiveret på dette emne."
+ bot: "bot"
+ create: "Opret"
+ cancel: "Annuller"
+ cancel_reply: "Annuller svar"
+ chat_channels: "Kanaler"
+ channel_settings:
+ title: "Kanal indstillinger"
+ edit: "Rediger"
+ add: "Tilføj"
+ leave_channel: "Forlad kanal"
+ join: "Tilslut"
+ leave: "Forlad"
+ channel_archive:
+ title: "Arkivér Kanal"
+ retry: "Forsøg igen"
+ channel_open:
+ title: "Åbn Kanal"
+ channel_close:
+ title: "Luk Kanal"
+ channel_delete:
+ title: "Slet Kanal"
+ confirm_channel_name: "Indtast kanalnavn"
+ channels_list_popup:
+ browse: "Gennemse kanaler"
+ click_to_join: "Klik her for at se tilgængelige kanaler."
+ close: "Luk"
+ delete: "Slet"
+ edited: "redigeret"
+ muted: "stille!"
+ joined: "tilmeldt"
+ email_frequency:
+ never: "Aldrig"
+ enable: "Aktiver chat"
+ flag: "Rapportér"
+ invalid_access: "Du har ikke adgang til at se denne chatkanal"
+ invitation_notification: "%{username} inviterede dig til at deltage i en chatkanal"
+ in_reply_to: "Som svar til"
+ heading: "Chat"
+ join: "Tilslut"
+ new_messages: "nye beskeder"
+ mention_warning:
+ dismiss: "ignorer Alle"
+ reply: "Svar"
+ edit: "Rediger"
+ rebake_message: "Gendan HTML"
+ bookmark_message: "Bogmærk"
+ bookmark_message_edit: "Rediger Bogmærke"
+ save: "Gem"
+ select: "Vælg"
+ sounds:
+ none: "Ingen"
+ bell: "Klokke"
+ ding: "Ding"
+ title: "chat"
+ title_capitalized: "Chat"
+ upload: "Vedhæft en fil"
+ uploaded_files:
+ one: "%{count} fil"
+ other: "%{count} filer"
+ exit: "tilbage"
+ channel_status:
+ read_only_header: "Kanalen er skrivebeskyttet"
+ read_only: "Kun læsning"
+ archived_header: "Kanalen er arkiveret"
+ archived: "Arkiveret"
+ closed: "Lukket"
+ open_header: "Kanalen er åben"
+ open: "Åbn"
+ browse:
+ back: "Tilbage"
+ title: Kanaler
+ filter_all: Alle
+ filter_closed: Lukket
+ filter_archived: Arkiveret
+ chat_message_separator:
+ today: I dag
+ yesterday: I går
+ about_view:
+ title: Titel
+ description: Beskrivelse
+ channel_info:
+ back_to_channel: "Tilbage"
+ tabs:
+ about: Om
+ members: Brugere
+ settings: Indstillinger
+ direct_message_creator:
+ title: Ny Besked
+ prefix: "Til:"
+ channel_selector:
+ title: "Hop til kanal"
+ no_channels: "Ingen kanaler matcher din søgning"
+ create_channel:
+ choose_category:
+ label: "Vælg en kategori"
+ none: "vælg en..."
+ create: "Opret kanal"
+ description: "Beskrivelse (valgfrit)"
+ name: "Kanal navn"
+ type: "Type"
+ types:
+ category: "Kategori"
+ topic: "Emne"
+ reviewable:
+ type: "Chat besked"
+ reactions:
+ only_you: "Du reagerede med :%{emoji}:"
+ and_others: "Du, %{usernames} reagerede med :%{emoji}:"
+ only_others: "%{usernames} reagerede med :%{emoji}:"
+ others_and_more: "%{usernames} og %{more} andre reagerede med :%{emoji}:"
+ you_others_and_more: "Du, %{usernames} og %{more} andre reagerede med :%{emoji}:"
+ composer:
+ italic_text: "kursiv skrift"
+ bold_text: "fed skrift"
+ code_text: "kode tekst"
+ quote:
+ copy_success: "Chat-citat kopieret til udklipsholderen"
+ notification_levels:
+ never: "Aldrig"
+ settings:
+ follow: "Tilslut"
+ followed: "Tilmeldt"
+ notifications: "Notifikationer"
+ preview: "Forhåndsvisning"
+ save: "Gem"
+ saved: "Gemt"
+ unfollow: "Forlad"
+ admin:
+ title: "Chat"
+ incoming_webhooks:
+ back: "Tilbage"
+ description: "Beskrivelse"
+ delete: "Slet"
+ emoji: "Humørikon"
+ name: "Navn"
+ save: "Gem"
+ edit: "Redigér"
+ system: "system"
+ url: "URL"
+ username: "Brugernavn"
+ selection:
+ cancel: "Annuller"
+ quote_selection: "Citat i emne"
+ copy: "Kopier"
+ error: "Der opstod en fejl under flytning af chatbeskeder"
+ title: "Flyt chat til emne"
+ new_topic:
+ title: "Flyt til nyt emne"
+ existing_topic:
+ title: "Flyt til eksisterende emne"
+ new_message:
+ title: "Flyt til ny besked"
+ replying_indicator:
+ single_user: "%{username} skriver"
+ retention_reminders:
+ public: "Kanalhistorikken gemmes i %{days} dage."
+ dm: "Personlig chathistorik gemmes i %{days} dage."
+ topic_button_title: "Chat"
+ emoji_picker:
+ objects: "Objekter"
+ activities: "Aktiviteter"
+ flags: "Flag"
+ symbols: "Symboler"
+ draft_channel_screen:
+ header: "Ny Besked"
+ cancel: "Annuller"
+ notifications:
+ chat_invitation_html: "%{username} inviterede dig til at deltage i en chatkanal"
+ chat_quoted: "%{username} %{description}"
+ popup:
+ chat_mention:
+ direct_html: '%{username} nævnte dig i "%{channel}"'
+ other_html: '%{username} nævnte %{identifier} i "%{channel}"'
+ direct_message_chat_mention:
+ direct_html: "%{username} nævnte dig i personlig chat"
+ other_html: "%{username} nævnte %{identifier} i personlig chat"
+ chat_message: "Ny chatbesked"
+ chat_quoted: "%{username} citerede din chatbesked"
+ titles:
+ chat_mention: "Chat omtale"
+ chat_invitation: "Chat invitation"
+ chat_quoted: "Chat citeret"
+ action_codes:
+ chat:
+ enabled: '%{who} aktiverede %{when}'
+ disabled: "%{who} lukkede chat %{when}"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ title: Send chatbesked
+ fields:
+ chat_channel_id:
+ label: Chat kanal ID
+ message:
+ label: Meddelelse
+ sender:
+ label: Afsender
+ description: Standard er system
+ review:
+ types:
+ reviewable_chat_message:
+ flagged_by: "Markeret af"
+ keyboard_shortcuts_help:
+ chat:
+ title: "Chat"
+ keyboard_shortcuts:
+ switch_channel_arrows: "%{shortcut} Skift kanal"
diff --git a/plugins/chat/config/locales/client.de.yml b/plugins/chat/config/locales/client.de.yml
new file mode 100644
index 0000000000..5f387f5c5b
--- /dev/null
+++ b/plugins/chat/config/locales/client.de.yml
@@ -0,0 +1,449 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+de:
+ js:
+ admin:
+ logs:
+ staff_actions:
+ actions:
+ chat_channel_status_change: "Chat-Kanal-Status geändert"
+ chat_channel_delete: "Chat-Kanal gelöscht"
+ api:
+ scopes:
+ descriptions:
+ chat:
+ create_message: "Erstelle eine Chat-Nachricht in einem bestimmten Kanal."
+ about:
+ chat_messages_count: "Chat-Nachrichten"
+ chat_channels_count: "Chat-Kanäle"
+ chat_users_count: "Chat-Benutzer"
+ chat:
+ dates:
+ time_tiny: "h:mm"
+ all_loaded: "Alle Nachrichten werden angezeigt"
+ already_enabled: "Der Chat ist für dieses Thema bereits aktiviert. Bitte aktualisieren."
+ disabled_for_topic: "Der Chat ist für dieses Thema deaktiviert."
+ bot: "Bot"
+ create: "Erstellen"
+ cancel: "Abbrechen"
+ cancel_reply: "Antwort verwerfen"
+ chat_channels: "Kanäle"
+ browse_all_channels: "Alle Kanäle durchsuchen"
+ move_to_channel:
+ title: "Nachrichten in Kanal verschieben"
+ instructions:
+ one: "Du verschiebst %{count} Nachricht. Wähle einen Zielkanal aus. Im Kanal %{channelTitle} wird eine Platzhalter-Nachricht erstellt, um anzuzeigen, dass diese Nachricht verschoben wurde."
+ other: "Du verschiebst %{count} Nachrichten. Wähle einen Zielkanal aus. Im Kanal %{channelTitle} wird eine Platzhalter-Nachricht erstellt, um anzuzeigen, dass diese Nachrichten verschoben wurden."
+ confirm_move: "Nachrichten verschieben"
+ channel_settings:
+ title: "Kanaleinstellungen"
+ edit: "Bearbeiten"
+ add: "Hinzufügen"
+ close_channel: "Kanal schließen"
+ open_channel: "Kanal öffnen"
+ archive_channel: "Kanal archivieren"
+ delete_channel: "Kanal löschen"
+ join_channel: "Kanal beitreten"
+ leave_channel: "Kanal verlassen"
+ join: "Beitreten"
+ leave: "Verlassen"
+ channel_archive:
+ title: "Kanal archivieren"
+ instructions: "/hooks/:key in einem bestimmten Chat-Kanal zu posten. Die Payload besteht aus einem einzigen text-Parameter, der auf 2000 Zeichen begrenzt ist.
Wir unterstützen auch in begrenztem Umfang Slack-formatierte text-Parameter und extrahieren Links und Erwähnungen basierend auf dem Format unter https://api.slack.com/reference/surfaces/formatting, aber dazu muss der Endpunkt /hooks/:key/slack verwendet werden."
+ selection:
+ cancel: "Abbrechen"
+ quote_selection: "Zitat im Thema"
+ copy: "Kopieren"
+ move_selection_to_channel: "In Kanal verschieben"
+ error: "Beim Verschieben der Chat-Nachrichten ist ein Fehler aufgetreten"
+ title: "Chat in Thema verschieben"
+ new_topic:
+ title: "In neues Thema verschieben"
+ instructions:
+ one: "Du bist dabei, ein neues Thema zu erstellen und es mit der ausgewählten Chat-Nachricht zu füllen."
+ other: "Du bist dabei, ein neues Thema zu erstellen und es mit den %{count} ausgewählten Chat-Nachrichten zu füllen."
+ instructions_channel_archive: "Du bist dabei, ein neues Thema zu erstellen und die Kanalnachrichten darin zu archivieren."
+ existing_topic:
+ title: "In bestehendes Thema verschieben"
+ instructions:
+ one: "Bitte wähle das Thema aus, in das du die Chat-Nachricht verschieben möchtest."
+ other: "Bitte wähle das Thema aus, in das du die %{count} Chat-Nachrichten verschieben möchtest."
+ instructions_channel_archive: "Bitte wähle das Thema aus, in dem du die Kanalnachrichten archivieren möchtest."
+ new_message:
+ title: "In neue Nachricht verschieben"
+ instructions:
+ one: "Du bist dabei, eine neue Nachricht zu erstellen und sie mit der ausgewählten Chat-Nachricht zu füllen."
+ other: "Du bist dabei, eine neue Nachricht zu erstellen und sie mit den %{count} ausgewählten Chat-Nachrichten zu füllen."
+ replying_indicator:
+ single_user: "%{username} schreibt"
+ multiple_users: "%{commaSeparatedUsernames} und %{lastUsername} schreiben"
+ many_users:
+ one: "%{commaSeparatedUsernames} und %{count} andere Person schreiben"
+ other: "%{commaSeparatedUsernames} und %{count} andere Personen schreiben"
+ retention_reminders:
+ public: "Der Kanalverlauf wird für %{days} Tage gespeichert."
+ dm: "Der persönliche Chatverlauf wird für %{days} Tage gespeichert."
+ topic_button_title: "Chat"
+ flags:
+ off_topic: "Diese Nachricht ist für die aktuelle Diskussion im Sinne des Kanaltitels nicht relevant und sollte wahrscheinlich an eine andere Stelle verschoben werden."
+ inappropriate: "Diese Nachricht enthält Inhalte, die eine vernünftige Person als anstößig, beleidigend oder als Verstoß gegen unsere Community-Richtlinien ansehen würde."
+ spam: "Diese Nachricht ist Werbung oder Vandalismus. Sie ist nicht nützlich oder relevant für den aktuellen Kanal."
+ notify_user: "Ich möchte mit dieser Person direkt und persönlich über ihre Nachricht sprechen."
+ notify_moderators: "Diese Nachricht erfordert die Aufmerksamkeit des Teams aus einem anderen, oben nicht aufgeführten Grund."
+ flagging:
+ action: "Nachricht markieren"
+ emoji_picker:
+ favorites: "Häufig verwendet"
+ smileys_&_emotion: "Smileys und Emotionen"
+ objects: "Objekte"
+ people_&_body: "Mensch und Körper"
+ travel_&_places: "Reisen und Orte"
+ animals_&_nature: "Tiere und Natur"
+ food_&_drink: "Essen und Trinken"
+ activities: "Aktivitäten"
+ flags: "Flaggen"
+ symbols: "Symbole"
+ search_placeholder: "Nach Emoji-Namen und -Alias suchen …"
+ no_results: "Keine Ergebnisse"
+ draft_channel_screen:
+ header: "Neue Nachricht"
+ cancel: "Abbrechen"
+ notifications:
+ chat_invitation: "hat dich eingeladen, einem Chat-Kanal beizutreten"
+ chat_invitation_html: "%{username} hat dich eingeladen, einem Chat-Kanal beizutreten"
+ chat_quoted: "%{username} %{description}"
+ popup:
+ chat_mention:
+ direct: 'hat dich in „%{channel}“ erwähnt'
+ direct_html: '%{username} hat dich in „%{channel}“ erwähnt'
+ other_plain: 'hat %{identifier} in „%{channel}“ erwähnt'
+ other_html: '%{username} hat %{identifier} in „%{channel}“ erwähnt'
+ direct_message_chat_mention:
+ direct: "hat dich im persönlichen Chat erwähnt"
+ direct_html: "%{username} hat dich im persönlichen Chat erwähnt"
+ other_plain: "hat %{identifier} im persönlichen Chat erwähnt"
+ other_html: "%{username} hat %{identifier} im persönlichen Chat erwähnt"
+ chat_message: "Neue Chat-Nachricht"
+ chat_quoted: "%{username} hat deine Chat-Nachricht zitiert"
+ titles:
+ chat_mention: "Chat-Erwähnung"
+ chat_invitation: "Chat-Einladung"
+ chat_quoted: "Chat zitiert"
+ action_codes:
+ chat:
+ enabled: '%{who} hat aktiviert %{when}'
+ disabled: "%{who} hat Chat geschlossen %{when}"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ title: Chat-Nachricht senden
+ fields:
+ chat_channel_id:
+ label: Chat-Kanal-ID
+ message:
+ label: Nachricht
+ sender:
+ label: Absender
+ description: Standardmäßig System
+ review:
+ transcript:
+ view: "Transkript früherer Nachrichten anzeigen"
+ types:
+ reviewable_chat_message:
+ title: "Chat-Nachricht markiert"
+ flagged_by: "Markiert von"
+ keyboard_shortcuts_help:
+ chat:
+ title: "Chat"
+ keyboard_shortcuts:
+ switch_channel_arrows: "%{shortcut} Kanal wechseln"
+ open_quick_channel_selector: "%{shortcut} Schnellauswahl für Kanäle öffnen"
+ open_insert_link_modal: "%{shortcut} Hyperlink einfügen (nur Composer)"
+ composer_bold: "%{shortcut} Fett (nur Composer)"
+ composer_italic: "%{shortcut} Kursiv (nur Composer)"
+ composer_code: "%{shortcut} Code (nur Composer)"
+ drawer_open: "%{shortcut} Chat-Bereich öffnen"
+ drawer_close: "%{shortcut} Chat-Bereich schließen"
+ topic_statuses:
+ chat:
+ help: "Der Chat ist für dieses Thema aktiviert"
+ user:
+ allow_private_messages: "Anderen Benutzern erlauben, mir persönliche Nachrichten und Chat-Direktnachrichten zu senden"
+ muted_users_instructions: "Alle Benachrichtigungen, persönlichen Nachrichten und Chat-Direktnachrichten von diesen Benutzern unterdrücken."
+ allowed_pm_users_instructions: "Nur persönliche Nachrichten oder Chat-Direktnachrichten von diesen Benutzern erlauben."
+ allow_private_messages_from_specific_users: "Nur bestimmten Benutzern erlauben, mir persönliche Nachrichten oder Chat-Direktnachrichten zu senden"
+ ignored_users_instructions: "Alle Beiträge, Nachrichten, Benachrichtigungen, persönlichen Nachrichten und Chat-Direktnachrichten von diesen Benutzern unterdrücken."
+ user_menu:
+ no_chat_notifications_title: "Du hast noch keine Chat-Benachrichtigungen"
+ no_chat_notifications_body: >
+ In diesem Bereich wirst du benachrichtigt, wenn dir jemand eine Direktnachricht sendet oder dich im Chat per @ erwähnt. Außerdem werden Benachrichtigungen an deine E-Mail-Adresse geschickt, wenn du dich eine Weile nicht angemeldet hast.
Klicke auf den Titel oben in einem Chat-Kanal, um festzulegen, welche Benachrichtigungen du in diesem Kanal erhältst. Weiteres findest du in deinen Benachrichtigungseinstellungen.
+ tabs:
+ chat_notifications: "Chat-Benachrichtigungen"
+ chat_notifications_with_unread:
+ one: "Chat-Benachrichtigungen – %{count} ungelesene Benachrichtigung"
+ other: "Chat-Benachrichtigungen – %{count} ungelesene Benachrichtigungen"
diff --git a/plugins/chat/config/locales/client.el.yml b/plugins/chat/config/locales/client.el.yml
new file mode 100644
index 0000000000..595dd09798
--- /dev/null
+++ b/plugins/chat/config/locales/client.el.yml
@@ -0,0 +1,115 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+el:
+ js:
+ chat:
+ create: "Δημιουργία"
+ cancel: "Ακύρωση"
+ channel_settings:
+ edit: "Επεξεργασία"
+ add: "Προσθήκη"
+ join: "Γίνετε μέλος"
+ leave: "Αποχώρηση"
+ close: "Κλείσιμο"
+ delete: "Σβήσιμο"
+ edited: "επεξεργάστηκε"
+ muted: "σε σίγαση"
+ joined: "έγινε μέλος"
+ email_frequency:
+ never: "Ποτέ"
+ flag: "Επισήμανση"
+ join: "Γίνετε μέλος"
+ mention_warning:
+ dismiss: "απόρριψη"
+ reply: "Απάντηση"
+ edit: "Επεξεργασία"
+ rebake_message: "Ανανέωση HTML"
+ bookmark_message: "Σελιδοδείκτης"
+ save: "Αποθήκευση"
+ sounds:
+ none: "Κανένα"
+ exit: "πίσω"
+ channel_status:
+ closed: "Κλειστό"
+ open: "Ξεκίνημα"
+ browse:
+ back: "Πίσω"
+ filter_all: Όλα
+ filter_closed: Κλειστό
+ chat_message_separator:
+ today: Σήμερα
+ yesterday: Χτες
+ about_view:
+ title: Τίτλος
+ description: Περιγραφή
+ channel_info:
+ back_to_channel: "Πίσω"
+ tabs:
+ about: Σχετικά
+ members: Μέλη
+ settings: Ρυθμίσεις
+ direct_message_creator:
+ title: Νέο Μήνυμα
+ prefix: "Προς:"
+ create_channel:
+ type: "Τύπος"
+ types:
+ category: "Κατηγορία"
+ topic: "Νήμα"
+ composer:
+ italic_text: "κείμενο σε έμφαση"
+ bold_text: "έντονη γραφή"
+ notification_levels:
+ never: "Ποτέ"
+ settings:
+ follow: "Γίνετε μέλος"
+ followed: "Έγινε μέλος"
+ notifications: "Ειδοποιήσεις"
+ preview: "Προεπισκόπηση"
+ save: "Αποθήκευση"
+ saved: "Αποθηκεύτηκε! "
+ unfollow: "Αποχώρηση"
+ incoming_webhooks:
+ back: "Πίσω"
+ description: "Περιγραφή"
+ delete: "Σβήσιμο"
+ emoji: "Emoji"
+ name: "Όνομα"
+ save: "Αποθήκευση"
+ edit: "Επεξεργασία"
+ system: "σύστημα"
+ url: "URL"
+ username: "Όνομα Χρήστη"
+ selection:
+ cancel: "Ακύρωση"
+ copy: "Αντιγραφή"
+ new_topic:
+ title: "Μεταφορά σε Νέο Νήμα "
+ existing_topic:
+ title: "Μεταφορά σε Υφιστάμενο Νήμα"
+ new_message:
+ title: "Μετακίνηση σε νέο μήνυμα"
+ emoji_picker:
+ objects: "Αντικείμενα"
+ activities: "Δραστηριότητες"
+ flags: "Σημάνσεις"
+ symbols: "Σύμβολα"
+ draft_channel_screen:
+ header: "Νέο Μήνυμα"
+ cancel: "Ακύρωση"
+ notifications:
+ chat_quoted: "%{username} %{description}"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ fields:
+ message:
+ label: Μήνυμα
+ review:
+ types:
+ reviewable_chat_message:
+ flagged_by: "Επισήμανση από"
diff --git a/plugins/chat/config/locales/client.en.yml b/plugins/chat/config/locales/client.en.yml
new file mode 100644
index 0000000000..a96c01e29f
--- /dev/null
+++ b/plugins/chat/config/locales/client.en.yml
@@ -0,0 +1,485 @@
+en:
+ js:
+ admin:
+ site_settings:
+ categories:
+ chat: Chat
+ logs:
+ staff_actions:
+ actions:
+ chat_channel_status_change: "Chat channel status changed"
+ chat_channel_delete: "Chat channel deleted"
+ api:
+ scopes:
+ descriptions:
+ chat:
+ create_message: "Create a chat message in a specified channel."
+ about:
+ chat_messages_count: "Chat Messages"
+ chat_channels_count: "Chat Channels"
+ chat_users_count: "Chat Users"
+
+ chat:
+ dates:
+ time_tiny: "h:mm"
+ all_loaded: "Showing all messages"
+ already_enabled: "Chat is already enabled on this topic. Please refresh."
+ disabled_for_topic: "Chat is disabled on this topic."
+ bot: "bot"
+ create: "Create"
+ cancel: "Cancel"
+ cancel_reply: "Cancel reply"
+ chat_channels: "Channels"
+ browse_all_channels: "Browse all channels"
+ move_to_channel:
+ title: "Move messages to channel"
+ instructions:
+ one: "You are moving %{count} message. Select a destination channel. A placeholder message will be created in the %{channelTitle} channel to indicate that this message has been moved."
+ other: "You are moving %{count} messages. Select a destination channel. A placeholder message will be created in the %{channelTitle} channel to indicate that these messages have been moved."
+ confirm_move: "Move Messages"
+ channel_settings:
+ title: "Channel settings"
+ edit: "Edit"
+ add: "Add"
+ close_channel: "Close channel"
+ open_channel: "Open channel"
+ archive_channel: "Archive channel"
+ delete_channel: "Delete channel"
+ join_channel: "Join channel"
+ leave_channel: "Leave channel"
+ join: "Join"
+ leave: "Leave"
+ save_label:
+ mute_channel: "Mute channel preference saved"
+ desktop_notification: "Desktop notification preference saved"
+ mobile_notification: "Mobile push notification preference saved"
+ channel_archive:
+ title: "Archive Channel"
+ instructions: "/hooks/:key endpoint. The payload consists of a single text parameter, which is limited to 2000 characters.
We also support limited Slack-formatted text parameters, extracting links and mentions based on the format at https://api.slack.com/reference/surfaces/formatting, but the /hooks/:key/slack endpoint must be used for this."
+
+ selection:
+ cancel: "Cancel"
+ quote_selection: "Quote in Topic"
+ copy: "Copy"
+ move_selection_to_channel: "Move to Channel"
+ error: "There was an error moving the chat messages"
+ title: "Move Chat to Topic"
+ new_topic:
+ title: "Move to New Topic"
+ instructions:
+ one: "You are about to create a new topic and populate it with the chat message you've selected."
+ other: "You are about to create a new topic and populate it with the %{count} chat messages you've selected."
+ instructions_channel_archive: "You are about to create a new topic and archive the channel messages to it."
+ existing_topic:
+ title: "Move to Existing Topic"
+ instructions:
+ one: "Please choose the topic you'd like to move that chat message to."
+ other: "Please choose the topic you'd like to move those %{count} chat messages to."
+ instructions_channel_archive: "Please choose the topic you'd like to archive the channel messages to."
+ new_message:
+ title: "Move to New Message"
+ instructions:
+ one: "You are about to create a new message and populate it with the chat message you've selected."
+ other: "You are about to create a new message and populate it with the %{count} chat messages you've selected."
+
+ replying_indicator:
+ single_user: "%{username} is typing"
+ multiple_users: "%{commaSeparatedUsernames} and %{lastUsername} are typing"
+ many_users:
+ one: "%{commaSeparatedUsernames} and %{count} other are typing"
+ other: "%{commaSeparatedUsernames} and %{count} others are typing"
+
+ retention_reminders:
+ public: "Channel history is retained for %{days} days."
+ dm: "Personal chat history is retained for %{days} days."
+
+ flags:
+ off_topic: "This message is not relevant to the current discussion as defined by the channel title, and should probably be moved elsewhere."
+ inappropriate: "This message contains content that a reasonable person would consider offensive, abusive, or a violation of our community guidelines."
+ spam: "This message is an advertisement, or vandalism. It is not useful or relevant to the current channel."
+ notify_user: "I want to talk to this person directly and personally about their message."
+ notify_moderators: "This message requires staff attention for another reason not listed above."
+
+ flagging:
+ action: "Flag message"
+
+ emoji_picker:
+ favorites: "Frequently used"
+ smileys_&_emotion: "Smileys and emotion"
+ objects: "Objects"
+ people_&_body: "People and body"
+ travel_&_places: "Travel and places"
+ animals_&_nature: "Animals and nature"
+ food_&_drink: "Food and drink"
+ activities: "Activities"
+ flags: "Flags"
+ symbols: "Symbols"
+ search_placeholder: "Search by emoji name and alias..."
+ no_results: "No results"
+
+ draft_channel_screen:
+ header: "New Message"
+ cancel: "Cancel"
+ notifications:
+ chat_invitation: "invited you to join a chat channel"
+ chat_invitation_html: "%{username} invited you to join a chat channel"
+ chat_quoted: "%{username} %{description}"
+
+ popup:
+ chat_mention:
+ direct: 'mentioned you in "%{channel}"'
+ direct_html: '%{username} mentioned you in "%{channel}"'
+ other_plain: 'mentioned %{identifier} in "%{channel}"'
+ other_html: '%{username} mentioned %{identifier} in "%{channel}"'
+ direct_message_chat_mention:
+ direct: "mentioned you in personal chat"
+ direct_html: "%{username} mentioned you in personal chat"
+ other_plain: "mentioned %{identifier} in personal chat"
+ other_html: "%{username} mentioned %{identifier} in personal chat"
+ chat_message: "New chat message"
+ chat_quoted: "%{username} quoted your chat message"
+
+ titles:
+ chat_mention: "Chat mention"
+ chat_invitation: "Chat invitation"
+ chat_quoted: "Chat quoted"
+ action_codes:
+ chat:
+ enabled: '%{who} enabled %{when}'
+ disabled: "%{who} closed chat %{when}"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ title: Send chat message
+ fields:
+ chat_channel_id:
+ label: Chat channel ID
+ message:
+ label: Message
+ sender:
+ label: Sender
+ description: Defaults to system
+ review:
+ transcript:
+ view: "View previous messages transcript"
+ types:
+ reviewable_chat_message:
+ title: "Flagged Chat Message"
+ flagged_by: "Flagged By"
+ keyboard_shortcuts_help:
+ chat:
+ title: "Chat"
+ keyboard_shortcuts:
+ switch_channel_arrows: "%{shortcut} Switch channel"
+ open_quick_channel_selector: "%{shortcut} Open quick channel selector"
+ open_insert_link_modal: "%{shortcut} Insert hyperlink (composer only)"
+ composer_bold: "%{shortcut} Bold (composer only)"
+ composer_italic: "%{shortcut} Italic (composer only)"
+ composer_code: "%{shortcut} Code (composer only)"
+ drawer_open: "%{shortcut} Open chat drawer"
+ drawer_close: "%{shortcut} Close chat drawer"
+ topic_statuses:
+ chat:
+ help: "Chat is enabled for this topic"
+ user:
+ allow_private_messages: "Allow other users to send me personal messages and chat direct messages"
+ muted_users_instructions: "Suppress all notifications, personal messages, and chat direct messages from these users."
+ allowed_pm_users_instructions: "Only allow personal messages or chat direct messages from these users."
+ allow_private_messages_from_specific_users: "Only allow specific users to send me personal messages or chat direct messages"
+ ignored_users_instructions: "Suppress all posts, messages, notifications, personal messages, and chat direct messages from these users."
+ user_menu:
+ no_chat_notifications_title: "You don’t have any chat notifications yet"
+ no_chat_notifications_body: >
+ You will be notified in this panel when someone direct messages you or @mentions you in chat. Notifications will also be sent to your email when you haven’t logged in for a while.
+
+ Click the title at the top of any chat channel to configure what notifications you receive in that channel. For more, see your notification preferences.
+ tabs:
+ chat_notifications: "Chat notifications"
+ chat_notifications_with_unread:
+ one: "Chat notifications - %{count} unread notification"
+ other: "Chat notifications - %{count} unread notifications"
diff --git a/plugins/chat/config/locales/client.en_GB.yml b/plugins/chat/config/locales/client.en_GB.yml
new file mode 100644
index 0000000000..4e05263133
--- /dev/null
+++ b/plugins/chat/config/locales/client.en_GB.yml
@@ -0,0 +1,11 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+en_GB:
+ js:
+ chat:
+ composer:
+ italic_text: "emphasised text"
diff --git a/plugins/chat/config/locales/client.es.yml b/plugins/chat/config/locales/client.es.yml
new file mode 100644
index 0000000000..bd83e3fe72
--- /dev/null
+++ b/plugins/chat/config/locales/client.es.yml
@@ -0,0 +1,449 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+es:
+ js:
+ admin:
+ logs:
+ staff_actions:
+ actions:
+ chat_channel_status_change: "Se ha cambiado el estado del canal de chat"
+ chat_channel_delete: "Canal de chat eliminado"
+ api:
+ scopes:
+ descriptions:
+ chat:
+ create_message: "Crea un mensaje de chat en un canal especificado."
+ about:
+ chat_messages_count: "Mensajes de chat"
+ chat_channels_count: "Canales de chat"
+ chat_users_count: "Usuarios del chat"
+ chat:
+ dates:
+ time_tiny: "h:mm"
+ all_loaded: "Mostrando todos los mensajes"
+ already_enabled: "El chat ya está activado en este tema. Actualiza."
+ disabled_for_topic: "El chat está deshabilitado en este tema."
+ bot: "bot"
+ create: "Crear"
+ cancel: "Cancelar"
+ cancel_reply: "Cancelar respuesta"
+ chat_channels: "Canales"
+ browse_all_channels: "Buscar todos los canales"
+ move_to_channel:
+ title: "Mover los mensajes al canal"
+ instructions:
+ one: "Estás moviendo %{count} mensaje. Selecciona un canal de destino. Se creará un mensaje marcador de posición en el canal %{channelTitle} para indicar que se ha movido este mensaje."
+ other: "Estás moviendo %{count} mensajes. Selecciona un canal de destino. Se creará un mensaje marcador de posición en el canal %{channelTitle} para indicar que se han movido estos mensajes."
+ confirm_move: "Mover mensajes"
+ channel_settings:
+ title: "Ajustes del canal"
+ edit: "Editar"
+ add: "Añade"
+ close_channel: "Cerrar canal"
+ open_channel: "Abrir canal"
+ archive_channel: "Archivar canal"
+ delete_channel: "Eliminar canal"
+ join_channel: "Unirse al canal"
+ leave_channel: "Abandonar canal"
+ join: "Unirse"
+ leave: "Abandonar"
+ channel_archive:
+ title: "Archivar canal"
+ instructions: "/hooks/:key. La carga útil consiste en un único parámetro texto, que está limitado a 2000 caracteres.
También admitimos parámetros texto limitados con formato Slack, extrayendo enlaces y menciones basados en el formato en https://api.slack.com/reference/surfaces/formatting, pero para ello debe utilizarse el punto final /hooks/:key/slack."
+ selection:
+ cancel: "Cancelar"
+ quote_selection: "Cita en el Tema"
+ copy: "Copia"
+ move_selection_to_channel: "Pasar al canal"
+ error: "Se ha producido un error al mover los mensajes de chat"
+ title: "Mover chat a tema"
+ new_topic:
+ title: "Mover a nuevo tema"
+ instructions:
+ one: "Estás a punto de crear un nuevo tema y rellenarlo con el mensaje de chat que has seleccionado."
+ other: "Estás a punto de crear un nuevo tema y rellenarlo con los %{count} mensajes de chat que has seleccionado."
+ instructions_channel_archive: "Vas a crear un nuevo tema y archivar en él los mensajes del canal."
+ existing_topic:
+ title: "Mover a un tema existente"
+ instructions:
+ one: "Elige el tema al que quieres mover ese mensaje de chat."
+ other: "Elige el tema al que quieres mover esos %{count} mensajes de chat."
+ instructions_channel_archive: "Elige el tema en el que quieres archivar los mensajes del canal."
+ new_message:
+ title: "Mover a mensaje nuevo"
+ instructions:
+ one: "Estás a punto de crear un nuevo mensaje y rellenarlo con el mensaje de chat que has seleccionado."
+ other: "Estás a punto de crear un nuevo mensaje y rellenarlo con los %{count} mensajes de chat que has seleccionado."
+ replying_indicator:
+ single_user: "%{username} está escribiendo"
+ multiple_users: "%{commaSeparatedUsernames} y %{lastUsername} están escribiendo"
+ many_users:
+ one: "%{commaSeparatedUsernames} y %{count} más está escribiendo"
+ other: "%{commaSeparatedUsernames} y %{count} más están escribiendo"
+ retention_reminders:
+ public: "El historial del canal se conserva durante %{days} días."
+ dm: "El historial de chat personal se conserva durante %{days} días."
+ topic_button_title: "Chat"
+ flags:
+ off_topic: "Este mensaje no es relevante para la discusión actual, tal y como se define en el título del canal, y probablemente debería moverse a otro lugar."
+ inappropriate: "Este mensaje tiene un contenido que una persona razonable consideraría ofensivo, abusivo o que viola las directrices de nuestra comunidad."
+ spam: "Este mensaje es un anuncio o vandalismo. No es útil ni relevante para el canal actual."
+ notify_user: "Quiero hablar con esta persona directa y personalmente sobre su mensaje."
+ notify_moderators: "Este mensaje requiere la atención del personal por otra razón no mencionada anteriormente."
+ flagging:
+ action: "Denunciar mensaje"
+ emoji_picker:
+ favorites: "De uso frecuente"
+ smileys_&_emotion: "Sonrisas y emociones"
+ objects: "Objetos"
+ people_&_body: "Personas y cuerpo"
+ travel_&_places: "Viajes y lugares"
+ animals_&_nature: "Animales y naturaleza"
+ food_&_drink: "Comida y bebida"
+ activities: "Actividades"
+ flags: "Denuncias"
+ symbols: "Símbolos"
+ search_placeholder: "Busca por nombre de emoji y alias..."
+ no_results: "No hay resultados"
+ draft_channel_screen:
+ header: "Nuevo mensaje"
+ cancel: "Cancelar"
+ notifications:
+ chat_invitation: "te invitó a unirte a un canal de chat"
+ chat_invitation_html: "%{username} te ha invitado a unirte a un canal de chat"
+ chat_quoted: "%{username} %{description}"
+ popup:
+ chat_mention:
+ direct: 'te ha mencionado en «%{channel}»'
+ direct_html: '%{username} te mencionó en «%{channel}»'
+ other_plain: 'mencionó %{identifier} en «%{channel}»'
+ other_html: '%{username} mencionó %{identifier} en «%{channel}»'
+ direct_message_chat_mention:
+ direct: "te mencionó en el chat personal"
+ direct_html: "%{username} te mencionó en el chat personal"
+ other_plain: "mencionó %{identifier} en el chat personal"
+ other_html: "%{username} mencionó %{identifier} en el chat personal"
+ chat_message: "Nuevo mensaje de chat"
+ chat_quoted: "%{username} citó tu mensaje de chat"
+ titles:
+ chat_mention: "Mención de chat"
+ chat_invitation: "Invitación de chat"
+ chat_quoted: "Chat citado"
+ action_codes:
+ chat:
+ enabled: '%{who} habilitó el %{when}'
+ disabled: "%{who} cerró el chat %{when}"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ title: Enviar mensaje de chat
+ fields:
+ chat_channel_id:
+ label: ID del canal de chat
+ message:
+ label: Mensaje
+ sender:
+ label: Remitente
+ description: Valores predeterminados del sistema
+ review:
+ transcript:
+ view: "Ver la transcripción de los mensajes anteriores"
+ types:
+ reviewable_chat_message:
+ title: "Mensaje de chat denunciado"
+ flagged_by: "Denunciado por"
+ keyboard_shortcuts_help:
+ chat:
+ title: "Chat"
+ keyboard_shortcuts:
+ switch_channel_arrows: "%{shortcut} Cambiar de canal"
+ open_quick_channel_selector: "%{shortcut} Abrir selector rápido de canales"
+ open_insert_link_modal: "%{shortcut} Insertar hipervínculo (solo compositor)"
+ composer_bold: "%{shortcut} Negrita (solo compositor)"
+ composer_italic: "%{shortcut} Cursiva (solo compositor)"
+ composer_code: "%{shortcut} Código (solo compositor)"
+ drawer_open: "%{shortcut} Abrir el cajón del chat"
+ drawer_close: "%{shortcut} Cerrar cajón del chat"
+ topic_statuses:
+ chat:
+ help: "El chat está activado para este tema"
+ user:
+ allow_private_messages: "Permitir que otros usuarios me envíen mensajes personales y mensajes directos del chat"
+ muted_users_instructions: "Suprime todas las notificaciones, mensajes personales y mensajes directos del chat de estos usuarios."
+ allowed_pm_users_instructions: "Solo permitir mensajes personales o mensajes directos de estos usuarios."
+ allow_private_messages_from_specific_users: "Permitir solo a determinados usuarios que me envíen mensajes personales o mensajes directos del chat"
+ ignored_users_instructions: "Suprime todas las publicaciones, mensajes, notificaciones, mensajes personales y mensajes directos del chat de estos usuarios."
+ user_menu:
+ no_chat_notifications_title: "Todavía no tienes ninguna notificación del chat"
+ no_chat_notifications_body: >
+ Se te notificará en este panel cuando alguien te envíe un mensaje directo o te @mencione en el chat. También se enviarán notificaciones a tu correo electrónico cuando no te hayas conectado durante un tiempo.
Haz clic en el título de la parte superior de cualquier canal de chat para configurar las notificaciones que recibes en ese canal. Para más información, consulta tus preferencias de notificaciones.
+ tabs:
+ chat_notifications: "Notificaciones de chat"
+ chat_notifications_with_unread:
+ one: "Notificaciones del chat: %{count} notificación no leída"
+ other: "Notificaciones de chat: %{count} notificaciones no leídas"
diff --git a/plugins/chat/config/locales/client.et.yml b/plugins/chat/config/locales/client.et.yml
new file mode 100644
index 0000000000..1d105e6f8b
--- /dev/null
+++ b/plugins/chat/config/locales/client.et.yml
@@ -0,0 +1,108 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+et:
+ js:
+ chat:
+ create: "Loo"
+ cancel: "Tühista"
+ channel_settings:
+ edit: "Muuda"
+ add: "isa"
+ join: "Liitu"
+ leave: "Lahku"
+ close: "Sulge"
+ delete: "Kustuta"
+ muted: "vaigistatud"
+ joined: "liitus"
+ email_frequency:
+ never: "Mitte kunagi"
+ flag: "Tähis"
+ join: "Liitu"
+ mention_warning:
+ dismiss: "ignoreeri"
+ reply: "Vasta"
+ edit: "Muuda"
+ rebake_message: "Rekonstrueeri HTML"
+ bookmark_message: "Järjehoidja"
+ save: "Salvesta"
+ sounds:
+ none: "Pole"
+ exit: "tagasi"
+ channel_status:
+ closed: "Suletud"
+ open: "Ava"
+ browse:
+ back: "Tagasi"
+ filter_all: Kõik
+ filter_closed: Suletud
+ chat_message_separator:
+ today: Täna
+ yesterday: Eile
+ about_view:
+ title: Pealkiri
+ description: Kirjeldus
+ channel_info:
+ back_to_channel: "Tagasi"
+ tabs:
+ about: Teave
+ members: Liikmed
+ settings: Sätted
+ direct_message_creator:
+ title: Uus sõnum
+ prefix: "Kellele:"
+ create_channel:
+ type: "Tüüp"
+ types:
+ category: "Foorum"
+ topic: "Teema"
+ composer:
+ italic_text: "esiletõstetud tekst"
+ bold_text: "rasvane tekst"
+ notification_levels:
+ never: "Mitte kunagi"
+ settings:
+ follow: "Liitu"
+ followed: "Liitus"
+ notifications: "Teavitus"
+ preview: "Eelvaade"
+ save: "Salvesta"
+ saved: "Salvestatud"
+ unfollow: "Lahku"
+ incoming_webhooks:
+ back: "Tagasi"
+ description: "Kirjeldus"
+ delete: "Kustuta"
+ emoji: "Emotikon"
+ name: "Nimi"
+ save: "Salvesta"
+ edit: "Muuda"
+ system: "süsteem"
+ url: "URL"
+ username: "Kasutajanimi"
+ selection:
+ cancel: "Tühista"
+ copy: "Kopeeri"
+ new_topic:
+ title: "Liiguta uue teema alla"
+ existing_topic:
+ title: "Liiguta olemasolevasse teemasse"
+ new_message:
+ title: "Liiguta uude sõnumisse"
+ emoji_picker:
+ objects: "Objektid"
+ flags: "Tähised"
+ draft_channel_screen:
+ header: "Uus sõnum"
+ cancel: "Tühista"
+ notifications:
+ chat_quoted: "%{username} %{description}"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ fields:
+ message:
+ label: Sõnum
diff --git a/plugins/chat/config/locales/client.fa_IR.yml b/plugins/chat/config/locales/client.fa_IR.yml
new file mode 100644
index 0000000000..706ac0f044
--- /dev/null
+++ b/plugins/chat/config/locales/client.fa_IR.yml
@@ -0,0 +1,334 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+fa_IR:
+ js:
+ admin:
+ logs:
+ staff_actions:
+ actions:
+ chat_channel_status_change: "وضعیت کانال گفتگو تغییر کرد"
+ chat_channel_delete: "کانال گفتگو حذف شد"
+ about:
+ chat_messages_count: "پیامهای گفتگو"
+ chat_channels_count: "کانالهای گفتگو"
+ chat_users_count: "کاربران گفتگو"
+ chat:
+ dates:
+ time_tiny: "HH:mm"
+ all_loaded: "نمایش همه پیامها"
+ already_enabled: "گفتگو در حال حاضر در این موضوع فعال شده است. لطفا صفحه جاری را تازهسازی کنید."
+ disabled_for_topic: "گفتگو در این موضوع غیرفعال است."
+ bot: "ربات"
+ create: "ایجاد"
+ cancel: "انصراف"
+ cancel_reply: "لغو پاسخ"
+ chat_channels: "کانالها"
+ browse_all_channels: "مرور همه کانالها"
+ move_to_channel:
+ title: "انتقال پیامها به کانال"
+ confirm_move: "انتقال پیامها"
+ channel_settings:
+ title: "تنظیمات کانال"
+ edit: "ویرایش"
+ add: "اضافه کردن"
+ close_channel: "بستن کانال"
+ open_channel: "باز کردن کانال"
+ archive_channel: "بایگانی کانال"
+ delete_channel: "حذف کانال"
+ join_channel: "پیوستن به کانال"
+ leave_channel: "ترک کانال"
+ join: "عضو شدن"
+ leave: "ترک کردن"
+ channel_archive:
+ title: "بایگانی کانال"
+ retry: "تلاش مجدد"
+ channel_open:
+ title: "باز کردن کانال"
+ instructions: "کانال را باز میکند، همه کاربران قادر خواهند بود پیام جدید ارسال کنند و پیامهای قبلی خود را ویرایش کنند."
+ channel_close:
+ title: "بستن کانال"
+ instructions: "بستن کانال از ارسال پیامهای جدید یا ویرایش پیامهای قبلی توسط کاربران که همکار نیستن جلوگیری میکند. آیا مطمئنید که میخواهید اين کانال رو ببنديد؟"
+ channel_delete:
+ title: "حذف کانال"
+ confirm_channel_name: "نام کانال را وارد کنید"
+ channels_list_popup:
+ browse: "مرور کانالها"
+ create: "کانال جدید"
+ click_to_join: "برای مشاهده کانالهای موجود، اینجا را کلیک کنید."
+ close: "بستن"
+ confirm_flag: "آیا برای پرچم گذاری پیام %{username} مطمئن هستید؟"
+ deleted: "یک پیام حذف شد. [view]"
+ hidden: "یک پیام پنهان شده است. [view]"
+ delete: "حذف"
+ edited: "ویرایش شده"
+ muted: "خاموش"
+ joined: "ملحق شده"
+ empty_state:
+ direct_message_cta: "شروع گفتگوی شخصی"
+ direct_message: "شما همچنین میتوانید یک گفتگو شخصی را با یک یا چند کاربر شروع کنید."
+ title: "هیچ کانالی پیدا نشد"
+ email_frequency:
+ never: "هرگز"
+ title: "آگاهسازیهای ایمیل"
+ enable: "فعال کردن گفتگو"
+ flag: "پرچم"
+ flagged: "این پیام برای بررسی پرچم گذاری شده است"
+ invalid_access: "شما برای مشاهده گفتگوی این کانال دسترسی ندارید"
+ in_reply_to: "در پاسخ به"
+ heading: "گفتگو"
+ join: "عضو شدن"
+ new_messages: "پیامهای جدید"
+ mention_warning:
+ dismiss: "رد کردن"
+ invitations_sent:
+ one: "دعوتنامه ارسال شد"
+ other: "دعوتنامهها ارسال شد"
+ invite: "دعوت به کانال"
+ without_membership:
+ one: "%{usernames} هنوز در این کانال عضو نشده است."
+ other: "%{usernames} هنوز در این کانال عضو نشده است."
+ aria_roles:
+ channels_list: "فهرست کانالهای گفتگو"
+ no_public_channels: "شما هنوز عضو هیچ کانالی نشدهاید."
+ open: "باز کردن گفتگو..."
+ close_full_page: "بستن گفتگو تمام صفحه"
+ open_message: "پیام را در گفتگو باز کن"
+ placeholder_start_conversation: شروع گفتگو با %{usernames}
+ remove_upload: "حذف پرونده"
+ react: "واکنش با شکلک"
+ reply: "پاسخ"
+ edit: "ویرایش"
+ copy_link: "کپی پیوند"
+ rebake_message: "ساخت مجدد HTML"
+ retry_staged_message:
+ title: "خطای شبکه"
+ action: "دوباره بفرستم؟"
+ unreliable_network: "شبکه پایدار نیست، ارسال پیامها و ذخیره پیشنویس ممکن است کار نکند"
+ bookmark_message: "نشانک"
+ bookmark_message_edit: "ویرایش نشانک"
+ restore: "بازگرداندن پیام حذف شده"
+ save: "ذخیره"
+ select: "انتخاب کنید"
+ return_to_list: "بازگشت به فهرست کانالها"
+ scroll_to_bottom: "حرکت به پایین"
+ scroll_to_new_messages: "مشاهده پیامهای جدید"
+ sound:
+ title: "صدای آگاهسازی گفتگوی دسکتاپ"
+ sounds:
+ none: "هیچ کدام"
+ bell: "زنگ"
+ ding: "دینگ"
+ title: "گفتگو"
+ title_capitalized: "گفتگو"
+ upload: "پیوست کردن یک پرونده"
+ uploaded_files:
+ one: "%{count} پرونده"
+ other: "%{count} پرونده"
+ you_flagged: "شما این پیام را پرچم گذاری کردید"
+ exit: "بازگشت"
+ channel_status:
+ read_only: "فقط خواندنی"
+ archived: "بایگانی شد"
+ closed_header: "کانال بسته است"
+ closed: "بسته"
+ open_header: "کانال باز است"
+ open: "باز"
+ browse:
+ back: "بازگشت"
+ title: کانالها
+ filter_all: همه
+ filter_open: باز شد
+ filter_closed: بسته
+ filter_archived: بایگانی شد
+ filter_input_placeholder: کانال را با نام جستجو کنید
+ chat_message_separator:
+ today: امروز
+ yesterday: دیروز
+ members_view:
+ filter_placeholder: جستجوی اعضا
+ about_view:
+ associated_topic: موضوع مرتبط
+ associated_category: دستهبندی مرتبط
+ title: عنوان
+ description: توضیح
+ channel_info:
+ back_to_all_channels: "همه کانالها"
+ back_to_channel: "بازگشت"
+ tabs:
+ about: درباره
+ members: اعضاء
+ settings: تنظیمات
+ channel_edit_title_modal:
+ title: ویرایش عنوان
+ input_placeholder: افزودن عنوان
+ description: یک عنوان توصیفی کوتاه به کانال خود بدهید
+ channel_edit_description_modal:
+ title: ویرایش توضیحات
+ input_placeholder: افزودن توضیحات
+ description: به بقیه افراد بگویید که این کانال در مورد چی هست
+ direct_message_creator:
+ title: پیام جدید
+ prefix: "به:"
+ no_results: هیج نتیجهای نداشت
+ channel_selector:
+ no_channels: "هیچ کانالی با جستجوی شما مطابقت ندارد"
+ channel:
+ no_memberships: این کانال هنوز هیچ عضوی ندارد
+ no_memberships_found: هیچ عضوی یافت نشد
+ memberships_count:
+ one: "%{count} عضو"
+ other: "%{count} عضو"
+ create_channel:
+ auto_join_users:
+ warning_groups:
+ one: به طور خودکار %{members_count} کاربر از گروه %{group} اضافه شود؟
+ other: به طور خودکار %{members_count} کاربر از گروه %{group} و %{group_2} اضافه شود؟
+ warning_multiple_groups: به طور خودکار %{members_count} کاربر از گروه %{group_1} و %{count} نفر دیگر اضافه شود؟
+ choose_category:
+ label: "انتخاب دستهبندی"
+ none: "یکی را انتخاب کنید..."
+ create: "ایجاد کانال"
+ description: "توضیحات «اختیاری»"
+ name: "نام کانال"
+ title: "کانال جدید"
+ type: "نوع"
+ types:
+ category: "دسته"
+ topic: "موضوعات"
+ reviewable:
+ type: "پیام گفتگو"
+ reactions:
+ only_you: "شما با :%{emoji}: واکنش نشان دادید"
+ composer:
+ italic_text: "متن تاکید شده"
+ bold_text: "نوشتهی ضخیم "
+ quote:
+ copy_success: "نقل قول گفتگو در کلیپبورد کپی شد"
+ notification_levels:
+ never: "هرگز"
+ mention: "فقط برای اشاره کردن"
+ always: "برای تمام فعالیتها"
+ settings:
+ desktop_notification_level: "آگاهسازیهای دسکتاپ"
+ follow: "عضو شدن"
+ followed: "عضو شده"
+ mute: "بیصدا کردن کانال"
+ muted_on: "روشن"
+ muted_off: "خاموش"
+ notifications: "اعلانها"
+ preview: "پیشنمایش"
+ save: "ذخیره کردن"
+ saved: "ذخیره شد"
+ unfollow: "ترک کردن"
+ admin:
+ title: "گفتگو"
+ direct_messages:
+ title: "گفتگوی شخصی"
+ new: "گفتگوی شخصی جدید"
+ create: "برو"
+ cannot_create: "با عرض پوزش، شما نمیتوانید پیام مستقیم ارسال کنید."
+ incoming_webhooks:
+ back: "بازگشت"
+ channel_placeholder: "یک کانال را انتخاب کنید"
+ current_emoji: "شکلک کنونی"
+ description: "توضیح"
+ delete: "پاک کردن"
+ emoji: "شکلک"
+ name: "نام"
+ name_placeholder: "نام..."
+ no_emoji: "هیچ شکلکی انتخاب نشده"
+ reset_emoji: "تنظیم مجدد شکلک"
+ save: "ذخیره کردن"
+ edit: "ویرایش"
+ select_emoji: "انتخاب شکلک"
+ system: "سیستم"
+ url: "آدرس"
+ username: "نامکاربری"
+ selection:
+ cancel: "انصراف"
+ quote_selection: "نقل قول در موضوع"
+ copy: "کپی"
+ move_selection_to_channel: "انتقال به کانال"
+ error: "در انتقال پیامهای گفتگو خطایی رخ داده"
+ title: "انتقال گفتگو به موضوع"
+ new_topic:
+ title: "انتقال به موضوع جدید"
+ existing_topic:
+ title: "انتقال به موضوع موجود"
+ new_message:
+ title: "انتقال به پیام جدید"
+ replying_indicator:
+ single_user: "%{username} در حال نوشتن"
+ multiple_users: "%{commaSeparatedUsernames} و %{lastUsername} در حال نوشتن"
+ topic_button_title: "گفتگو"
+ emoji_picker:
+ favorites: "اغلب استفاده میشه"
+ objects: "اشیا"
+ people_&_body: "مردم و بدن"
+ travel_&_places: "سفر و مکانها"
+ animals_&_nature: "حیوانات و طبیعت"
+ activities: "فعالیت ها"
+ flags: "پرچمها"
+ symbols: "نشانه ها"
+ no_results: "هیج نتیجهای نداشت"
+ draft_channel_screen:
+ header: "پیام جدید"
+ cancel: "انصراف"
+ notifications:
+ chat_quoted: "%{username} %{description}"
+ popup:
+ chat_message: "پیام گفتگو جدید"
+ chat_quoted: "%{username} پیام گفتگو شما را نقل کرد"
+ titles:
+ chat_mention: "اشاره گفتگو"
+ chat_invitation: "دعوتنامه گفتگو"
+ action_codes:
+ chat:
+ enabled: '%{who} فعال شد %{when}'
+ disabled: "%{who} گفتگو بسته شد %{when}"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ title: ارسال پیام
+ fields:
+ chat_channel_id:
+ label: شناسه کانال گفتگو
+ message:
+ label: پیام
+ sender:
+ label: فرستنده
+ description: پیشفرضهای سیستم
+ review:
+ transcript:
+ view: "مشاهده رونوشت متن پیامهای قبلی"
+ types:
+ reviewable_chat_message:
+ title: "پیام گفتگوی پرچم گذاری شده"
+ flagged_by: "پرچم شده توسط"
+ keyboard_shortcuts_help:
+ chat:
+ title: "گفتگو"
+ keyboard_shortcuts:
+ switch_channel_arrows: "%{shortcut} تغییر کانال"
+ drawer_open: "%{shortcut} باز کردن کِشوی گفتگو"
+ drawer_close: "%{shortcut} بستن کِشوی گفتگو"
+ topic_statuses:
+ chat:
+ help: "گفتگو برای این موضوع فعال است"
+ user:
+ allow_private_messages: "به دیگر، کاربران اجازه دهید پیامهای شخصی و پیامهای مستقیم در گفتگو را برای من ارسال کنند"
+ muted_users_instructions: "همه آگاهسازیها، پیامهای شخصی و پیامهای مستقیم در گفتگو از این کاربران را سرکوب کنید."
+ allowed_pm_users_instructions: "فقط پیامهای شخصی یا پیامهای مستقیم در گفتگو را از این کاربران اجازه دهید."
+ allow_private_messages_from_specific_users: "فقط به کاربران خاصی اجازه دهید، تا پیامهای شخصی یا پیامهای مستقیم در گفتگو را برای من ارسال کنند"
+ ignored_users_instructions: "تمام نوشتهها، آگاهسازیها، پیامهای شخصی و پیامهای مستقیم در گفتگو این کاربران را سرکوب کنید."
+ user_menu:
+ no_chat_notifications_title: "شما هنوز هیچ آگاهسازی گفتگوی ندارید"
+ tabs:
+ chat_notifications: "آگاهسازیهای گفتگو"
+ chat_notifications_with_unread:
+ one: "آگاهسازیهای گفتگو - %{count} آگاهسازی خوانده نشده"
+ other: "آگاهسازیهای گفتگو - %{count} آگاهسازی خوانده نشده"
diff --git a/plugins/chat/config/locales/client.fi.yml b/plugins/chat/config/locales/client.fi.yml
new file mode 100644
index 0000000000..aca5536ab5
--- /dev/null
+++ b/plugins/chat/config/locales/client.fi.yml
@@ -0,0 +1,449 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+fi:
+ js:
+ admin:
+ logs:
+ staff_actions:
+ actions:
+ chat_channel_status_change: "Chat-kanavan tila muuttui"
+ chat_channel_delete: "Chat-kanava poistettu"
+ api:
+ scopes:
+ descriptions:
+ chat:
+ create_message: "Luo chat-viesti tietyllä kanavalla."
+ about:
+ chat_messages_count: "Chat-viestit"
+ chat_channels_count: "Chat-kanavat"
+ chat_users_count: "Chat-käyttäjät"
+ chat:
+ dates:
+ time_tiny: "t:mm"
+ all_loaded: "Näytetään kaikki viestit"
+ already_enabled: "Chat on jo käytössä tässä ketjussa. Päivitä."
+ disabled_for_topic: "Chat on poistettu käytöstä tässä ketjussa."
+ bot: "botti"
+ create: "Luo"
+ cancel: "Peruuta"
+ cancel_reply: "Peruuta vastaus"
+ chat_channels: "Kanavat"
+ browse_all_channels: "Selaa kaikkia kanavia"
+ move_to_channel:
+ title: "Siirrä viestit kanavalle"
+ instructions:
+ one: "Olet siirtämässä %{count} viestin. Valitse kohdekanava. Kanavalle %{channelTitle} luodaan paikkamerkkiviesti, joka osoittaa, että tämä viesti on siirretty."
+ other: "Olet siirtämässä %{count} viestiä. Valitse kohdekanava. Kanavalle %{channelTitle} luodaan paikkamerkkiviesti, joka osoittaa, että nämä viestit on siirretty."
+ confirm_move: "Siirrä viestit"
+ channel_settings:
+ title: "Kanavan asetukset"
+ edit: "Muokkaa"
+ add: "Lisää"
+ close_channel: "Sulje kanava"
+ open_channel: "Avaa kanava"
+ archive_channel: "Arkistoi kanava"
+ delete_channel: "Poista kanava"
+ join_channel: "Liity kanavalle"
+ leave_channel: "Poistu kanavalta"
+ join: "Liity"
+ leave: "Poistu"
+ channel_archive:
+ title: "Arkistoi kanava"
+ instructions: "/hooks/:key-päätepisteen kautta. Hyötykuorma koostuu yhdestä text-parametrista, joka on rajoitettu 2 000 merkkiin.
Tuemme myös rajoitettuja Slack-muotoiltuja text-parametreja, linkkien ja mainintojen poimimista osoitteessa https://api.slack.com/reference/surfaces/formatting kuvatun muodon perusteella, mutta tähän täytyy käyttää /hooks/:key/ Slack-päätepistettä."
+ selection:
+ cancel: "Peruuta"
+ quote_selection: "Lainaus ketjussa"
+ copy: "Kopioi"
+ move_selection_to_channel: "Siirrä kanavalle"
+ error: "Chat-viestien siirtämisessä tapahtui virhe"
+ title: "Siirrä chat ketjuun"
+ new_topic:
+ title: "Siirrä uuteen ketjuun"
+ instructions:
+ one: "Olet luomassa uutta ketjua ja lisäämässä valitsemasi chat-viestin siihen."
+ other: "Olet luomassa uutta ketjua ja lisäämässä %{count} valitsemaasi chat-viestiä siihen."
+ instructions_channel_archive: "Olet luomassa uutta ketjua ja arkistoimassa kanavan viestit siihen."
+ existing_topic:
+ title: "Siirrä olemassa olevaan ketjuun"
+ instructions:
+ one: "Valitse ketju, johon haluat siirtää tämän chat-viestin."
+ other: "Valitse ketju, johon haluat siirtää nämä %{count} chat-viestiä."
+ instructions_channel_archive: "Valitse ketju, johon haluat arkistoida kanavan viestit."
+ new_message:
+ title: "Siirrä uuteen viestiin"
+ instructions:
+ one: "Olet luomassa uutta viestiä ja lisäämässä valitsemasi chat-viestin siihen."
+ other: "Olet luomassa uutta viestiä ja lisäämässä %{count} valitsemaasi chat-viestiä siihen."
+ replying_indicator:
+ single_user: "%{username} kirjoittaa"
+ multiple_users: "%{commaSeparatedUsernames} ja %{lastUsername} kirjoittavat"
+ many_users:
+ one: "%{commaSeparatedUsernames} ja %{count} muu kirjoittavat"
+ other: "%{commaSeparatedUsernames} ja %{count} muuta kirjoittavat"
+ retention_reminders:
+ public: "Kanavan historia säilytetään %{days} päivän ajan."
+ dm: "Henkilökohtainen keskusteluhistoria säilytetään %{days} päivän ajan."
+ topic_button_title: "Chat"
+ flags:
+ off_topic: "Tämä viesti ei liity nykyiseen keskusteluun kanavan otsikon mukaan, ja se pitäisi todennäköisesti siirtää muualle."
+ inappropriate: "Tämä viesti sisältää sisältöä, jota kohtuullinen henkilö pitäisi loukkaavana, herjaavana tai yhteisömme ohjeiden vastaisena."
+ spam: "Tämä viesti on mainos tai ilkivaltaa. Se ei ole hyödyllinen tai olennainen nykyiselle kanavalle."
+ notify_user: "Haluan keskustella tämän henkilön kanssa suoraan ja henkilökohtaisesti hänen viestistään."
+ notify_moderators: "Tämä viesti vaatii henkilökunnan huomion muusta syystä, jota ei ole mainittu edellä."
+ flagging:
+ action: "Liputa viesti"
+ emoji_picker:
+ favorites: "Usein käytetyt"
+ smileys_&_emotion: "Hymiöt ja tunteet"
+ objects: "Esineet"
+ people_&_body: "Ihmiset ja keho"
+ travel_&_places: "Matkailu ja paikat"
+ animals_&_nature: "Eläimet ja luonto"
+ food_&_drink: "Ruoka ja juoma"
+ activities: "Aktiviteetit"
+ flags: "Liput"
+ symbols: "Symbolit"
+ search_placeholder: "Hae emojin nimen ja aliaksen mukaan..."
+ no_results: "Ei tuloksia"
+ draft_channel_screen:
+ header: "Uusi viesti"
+ cancel: "Peruuta"
+ notifications:
+ chat_invitation: "kutsui sinut liittymään chat-kanavalle"
+ chat_invitation_html: "%{username} kutsui sinut liittymään chat-kanavalle"
+ chat_quoted: "%{username} %{description}"
+ popup:
+ chat_mention:
+ direct: 'mainitsi sinut kanavalla "%{channel}"'
+ direct_html: '%{username} mainitsi sinut kanavalla "%{channel}"'
+ other_plain: 'mainitsi kohteen %{identifier} kanavalla "%{channel}"'
+ other_html: '%{username} mainitsi kohteen %{identifier} kanavalla "%{channel}"'
+ direct_message_chat_mention:
+ direct: "mainitsi sinut henkilökohtaisessa chatissa"
+ direct_html: "%{username} mainitsi sinut henkilökohtaisessa chatissa"
+ other_plain: "mainitsi kohteen %{identifier} henkilökohtaisessa chatissa"
+ other_html: "%{username} mainitsi kohteen %{identifier} henkilökohtaisessa chatissa"
+ chat_message: "Uusi chat-viesti"
+ chat_quoted: "%{username} lainasi chat-viestiäsi"
+ titles:
+ chat_mention: "Chat-maininta"
+ chat_invitation: "Chat-kutsu"
+ chat_quoted: "Chat-keskustelua lainattu"
+ action_codes:
+ chat:
+ enabled: '%{who} otti käyttöön %{when}'
+ disabled: "%{who} sulki chatin %{when}"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ title: Lähetä chat-viesti
+ fields:
+ chat_channel_id:
+ label: Chat-kanavan tunnus
+ message:
+ label: Viesti
+ sender:
+ label: Lähettäjä
+ description: Oletuksena järjestelmä
+ review:
+ transcript:
+ view: "Näytä aiempien viestien transkriptio"
+ types:
+ reviewable_chat_message:
+ title: "Liputettu chat-viesti"
+ flagged_by: "Liputtanut"
+ keyboard_shortcuts_help:
+ chat:
+ title: "Chat"
+ keyboard_shortcuts:
+ switch_channel_arrows: "%{shortcut} Vaihda kanavaa"
+ open_quick_channel_selector: "%{shortcut} Avaa kanavan pikavalitsin"
+ open_insert_link_modal: "%{shortcut} Lisää hyperlinkki (vain tekstieditori)"
+ composer_bold: "%{shortcut} Lihavointi (vain tekstieditori)"
+ composer_italic: "%{shortcut} Kursiivi (vain tekstieditori)"
+ composer_code: "%{shortcut} Koodi (vain tekstieditori)"
+ drawer_open: "%{shortcut} Avaa chat-laatikko"
+ drawer_close: "%{shortcut} Sulje chat-laatikko"
+ topic_statuses:
+ chat:
+ help: "Chat on käytössä tässä ketjussa"
+ user:
+ allow_private_messages: "Salli muiden käyttäjien lähettää minulle yksityisviestejä ja chat-yksityisviestejä"
+ muted_users_instructions: "Estä kaikki ilmoitukset, yksityisviestit ja chat-yksityisviestit näiltä käyttäjiltä."
+ allowed_pm_users_instructions: "Salli vain näiden käyttäjien yksityisviestit tai chat-yksityisviestit."
+ allow_private_messages_from_specific_users: "Salli vain tiettyjen käyttäjien lähettää minulle yksityisviestejä tai chat-yksityisviestejä"
+ ignored_users_instructions: "Estä kaikki viestit, ilmoitukset, yksityisviestit ja chat-yksityisviestit näiltä käyttäjiltä."
+ user_menu:
+ no_chat_notifications_title: "Sinulla ei ole vielä chat-ilmoituksia"
+ no_chat_notifications_body: >
+ Saat ilmoituksen tässä paneelissa, kun joku lähettää sinulle viestin tai @mainitsee sinut chatissa. Ilmoitukset lähetetään myös sähköpostiisi, jos et ole kirjautunut sisään vähään aikaan.
Klikkaa minkä tahansa chat-kanavan yläosassa olevaa otsikkoa määrittääksesi, mitä ilmoituksia saat kyseisellä kanavalla. Katso lisätietoja ilmoitusasetuksistasi.
+ tabs:
+ chat_notifications: "Chat-ilmoitukset"
+ chat_notifications_with_unread:
+ one: "Chat-ilmoitukset – %{count} lukematon ilmoitus"
+ other: "Chat-ilmoitukset – %{count} lukematonta ilmoitusta"
diff --git a/plugins/chat/config/locales/client.fr.yml b/plugins/chat/config/locales/client.fr.yml
new file mode 100644
index 0000000000..3fd58cb732
--- /dev/null
+++ b/plugins/chat/config/locales/client.fr.yml
@@ -0,0 +1,449 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+fr:
+ js:
+ admin:
+ logs:
+ staff_actions:
+ actions:
+ chat_channel_status_change: "Le statut du canal de discussion a changé"
+ chat_channel_delete: "Canal de discussion supprimé"
+ api:
+ scopes:
+ descriptions:
+ chat:
+ create_message: "Créez un message instantané dans un canal spécifié."
+ about:
+ chat_messages_count: "Messages instantanés"
+ chat_channels_count: "Canaux de discussion"
+ chat_users_count: "Utilisateurs de la discussion"
+ chat:
+ dates:
+ time_tiny: "h:mm"
+ all_loaded: "Affichage de tous les messages"
+ already_enabled: "La discussion est déjà activée sur ce sujet. Veuillez actualiser."
+ disabled_for_topic: "La discussion est désactivée sur ce sujet."
+ bot: "robot"
+ create: "Créer"
+ cancel: "Annuler"
+ cancel_reply: "Annuler la réponse"
+ chat_channels: "Canaux"
+ browse_all_channels: "Parcourir tous les canaux"
+ move_to_channel:
+ title: "Déplacer les messages vers le canal"
+ instructions:
+ one: "Vous déplacez %{count} message. Sélectionnez un canal de destination. Un message de substitution sera créé dans le canal %{channelTitle} pour indiquer que ce message a été déplacé."
+ other: "Vous déplacez %{count} messages. Sélectionnez un canal de destination. Un message de substitution sera créé dans le canal %{channelTitle} pour indiquer que ces messages ont été déplacés."
+ confirm_move: "Déplacer les messages"
+ channel_settings:
+ title: "Paramètres du canal"
+ edit: "Modifier"
+ add: "Ajouter"
+ close_channel: "Fermer le canal"
+ open_channel: "Ouvrir le canal"
+ archive_channel: "Archiver le canal"
+ delete_channel: "Supprimer le canal"
+ join_channel: "Rejoindre le canal"
+ leave_channel: "Quitter le canal"
+ join: "Rejoindre"
+ leave: "Quitter"
+ channel_archive:
+ title: "Archiver le canal"
+ instructions: "/hooks/:key. La charge utile se compose d'un seul paramètre texte, qui est limité à 2000 caractères.
Nous prenons également en charge des paramètres de texte au format Slack limités, en extrayant des liens et des mentions sur la base du format https://api.slack.com/reference/surfaces/formatting, mais le point de terminaison /hooks/:key/slack doit être utilisé pour cela."
+ selection:
+ cancel: "Annuler"
+ quote_selection: "Citation dans le sujet"
+ copy: "Copie"
+ move_selection_to_channel: "Déplacer vers le canal"
+ error: "Une erreur s'est produite lors du déplacement des messages de la discussion"
+ title: "Déplacer la discussion vers le sujet"
+ new_topic:
+ title: "Déplacer vers un nouveau sujet"
+ instructions:
+ one: "Vous êtes sur le point de créer un nouveau sujet et de le remplir avec le message de discussion que vous avez sélectionné."
+ other: "Vous êtes sur le point de créer un nouveau sujet et de le remplir avec les %{count} messages de discussion que vous avez sélectionnés."
+ instructions_channel_archive: "Vous êtes sur le point de créer un nouveau sujet et d'y archiver les messages du canal."
+ existing_topic:
+ title: "Déplacer vers un sujet existant"
+ instructions:
+ one: "Veuillez choisir le sujet vers lequel vous souhaitez déplacer ce message de discussion."
+ other: "Veuillez choisir le sujet vers lequel vous souhaitez déplacer ces %{count} messages de discussion."
+ instructions_channel_archive: "Veuillez choisir le sujet dans lequel vous souhaitez archiver les messages du canal."
+ new_message:
+ title: "Déplacer vers un nouveau message"
+ instructions:
+ one: "Vous êtes sur le point de créer un nouveau message et de le remplir avec le message de discussion que vous avez sélectionné."
+ other: "Vous êtes sur le point de créer un nouveau message et de le remplir avec les %{count} messages de discussion que vous avez sélectionnés."
+ replying_indicator:
+ single_user: "%{username} est en train d'écrire"
+ multiple_users: "%{commaSeparatedUsernames} et %{lastUsername} sont en train d'écrire"
+ many_users:
+ one: "%{commaSeparatedUsernames} et %{count} autre utilisateur sont en train d'écrire"
+ other: "%{commaSeparatedUsernames} et %{count} autres utilisateurs sont en train d'écrire"
+ retention_reminders:
+ public: "L'historique du canal est conservé pendant %{days} jours."
+ dm: "L'historique des discussions privées est conservé pendant %{days} jours."
+ topic_button_title: "Discussion"
+ flags:
+ off_topic: "Ce message n'est pas pertinent pour la discussion en cours telle que définie par le titre du canal et devrait probablement être déplacé ailleurs."
+ inappropriate: "Ce message contient du contenu qu'une personne raisonnable considérerait comme offensant, abusif ou contraire à nos consignes communautaires."
+ spam: "Ce message est une publicité ou du vandalisme. Il n'est pas utile ou pertinent pour le canal actuel."
+ notify_user: "Je veux parler à cette personne directement et personnellement de son message."
+ notify_moderators: "Ce message requiert l'attention d'un responsable pour une autre raison non répertoriée ci-dessus."
+ flagging:
+ action: "Signaler un message"
+ emoji_picker:
+ favorites: "Fréquemment utilisé"
+ smileys_&_emotion: "Smileys et émotions"
+ objects: "Objets"
+ people_&_body: "Personnes et corps"
+ travel_&_places: "Voyages et lieux"
+ animals_&_nature: "Animaux et nature"
+ food_&_drink: "Nourriture et boissons"
+ activities: "Activités"
+ flags: "Drapeaux"
+ symbols: "Symboles"
+ search_placeholder: "Recherche par nom d'émoji et alias…"
+ no_results: "Aucun résultat"
+ draft_channel_screen:
+ header: "Nouveau message"
+ cancel: "Annuler"
+ notifications:
+ chat_invitation: "vous a invité(e) à rejoindre un canal de discussion"
+ chat_invitation_html: "%{username} vous a invité(e) à rejoindre un canal de discussion"
+ chat_quoted: "%{username} %{description}"
+ popup:
+ chat_mention:
+ direct: 'vous a mentionné(e) dans « %{channel} »'
+ direct_html: '%{username} vous a mentionné(e) dans « %{channel} »'
+ other_plain: 'a mentionné %{identifier} dans « %{channel} »'
+ other_html: '%{username} a mentionné %{identifier} dans « %{channel} »'
+ direct_message_chat_mention:
+ direct: "vous a mentionné(e) dans la discussion privée"
+ direct_html: "%{username} vous a mentionné(e) dans une discussion privée"
+ other_plain: "a mentionné %{identifier} dans une discussion privée"
+ other_html: "%{username} a mentionné %{identifier} dans une discussion privée"
+ chat_message: "Nouveau message de discussion"
+ chat_quoted: "%{username} a cité votre message de discussion"
+ titles:
+ chat_mention: "Mention de discussion"
+ chat_invitation: "Invitation à rejoindre la discussion"
+ chat_quoted: "Discussion citée"
+ action_codes:
+ chat:
+ enabled: '%{who} a activé la %{when}'
+ disabled: "%{who} a fermé la discussion %{when}"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ title: Envoyer un message de discussion
+ fields:
+ chat_channel_id:
+ label: ID du canal de discussion
+ message:
+ label: Message
+ sender:
+ label: Expéditeur
+ description: Paramètres par défaut
+ review:
+ transcript:
+ view: "Afficher la transcription des messages précédents"
+ types:
+ reviewable_chat_message:
+ title: "Message de discussion signalé"
+ flagged_by: "Signalé par"
+ keyboard_shortcuts_help:
+ chat:
+ title: "Discussion"
+ keyboard_shortcuts:
+ switch_channel_arrows: "%{shortcut} Changer de canal"
+ open_quick_channel_selector: "%{shortcut} Ouvrir le sélecteur de canal rapide"
+ open_insert_link_modal: "%{shortcut} Insérer un lien hypertexte (compositeur uniquement)"
+ composer_bold: "%{shortcut} Gras (compositeur uniquement)"
+ composer_italic: "%{shortcut} Italique (compositeur uniquement)"
+ composer_code: "%{shortcut} Code (compositeur uniquement)"
+ drawer_open: "%{shortcut} Ouvrir le tiroir de discussion"
+ drawer_close: "%{shortcut} Fermer le tiroir de discussion"
+ topic_statuses:
+ chat:
+ help: "La discussion est activée pour ce sujet"
+ user:
+ allow_private_messages: "Autoriser les autres utilisateurs à m'envoyer des messages privés et des messages directs de discussion"
+ muted_users_instructions: "Supprimer toutes les notifications, les messages privés et les messages directs de discussion de ces utilisateurs."
+ allowed_pm_users_instructions: "Autoriser uniquement les messages privés ou les messages directs de ces utilisateurs."
+ allow_private_messages_from_specific_users: "Autoriser uniquement des utilisateurs spécifiques à m'envoyer des messages privés ou des messages directs dans la discussion"
+ ignored_users_instructions: "Supprimer tous les messages, notifications, messages privés et messages directs de discussion de ces utilisateurs."
+ user_menu:
+ no_chat_notifications_title: "Vous n'avez pas encore reçu de notification de discussion"
+ no_chat_notifications_body: >
+ Vous serez averti(e) dans ce panneau lorsque quelqu'un vous enverra un message direct ou une @mention dans la discussion. Des notifications seront également envoyées à votre adresse e-mail si vous ne vous êtes pas connecté(e) pendant un certain temps.
Cliquez sur le titre en haut de n'importe quel canal de discussion pour configurer les notifications que vous recevez dans ce canal. Pour en savoir plus, consultez vos préférences de notification.
+ tabs:
+ chat_notifications: "Notifications de discussion"
+ chat_notifications_with_unread:
+ one: "Notifications de discussion - %{count} notification non lue"
+ other: "Notifications de discussion - %{count} notifications non lues"
diff --git a/plugins/chat/config/locales/client.gl.yml b/plugins/chat/config/locales/client.gl.yml
new file mode 100644
index 0000000000..dd8d5ba85c
--- /dev/null
+++ b/plugins/chat/config/locales/client.gl.yml
@@ -0,0 +1,115 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+gl:
+ js:
+ chat:
+ create: "Crear"
+ cancel: "Cancelar"
+ channel_settings:
+ edit: "Editar"
+ add: "Engadir"
+ join: "Participar"
+ leave: "Abandonar"
+ close: "Pechar"
+ delete: "Eliminar"
+ edited: "editado"
+ muted: "silenciado"
+ joined: "uniuse"
+ email_frequency:
+ never: "Nunca"
+ flag: "Sinalar"
+ join: "Participar"
+ mention_warning:
+ dismiss: "desbotar"
+ reply: "Responder"
+ edit: "Editar"
+ rebake_message: "Reconstruír HTML"
+ bookmark_message: "Marcador"
+ save: "Gardar"
+ sounds:
+ none: "Ningunha"
+ exit: "volver"
+ channel_status:
+ closed: "Pechado"
+ open: "Abrir"
+ browse:
+ back: "Volver"
+ filter_all: Todas
+ filter_closed: Pechado
+ chat_message_separator:
+ today: Hoxe
+ yesterday: Onte
+ about_view:
+ title: Título
+ description: Descrición
+ channel_info:
+ back_to_channel: "Volver"
+ tabs:
+ about: Verbo de
+ members: Membros
+ settings: Configuración
+ direct_message_creator:
+ title: Nova mensaxe
+ prefix: "A:"
+ create_channel:
+ type: "Tipo"
+ types:
+ category: "Categoría"
+ topic: "Tema"
+ composer:
+ italic_text: "texto recalcado"
+ bold_text: "texto groso"
+ notification_levels:
+ never: "Nunca"
+ settings:
+ follow: "Participar"
+ followed: "Uniuse"
+ notifications: "Notificacións"
+ preview: "Visualizar"
+ save: "Gardar"
+ saved: "Gardado"
+ unfollow: "Abandonar"
+ incoming_webhooks:
+ back: "Volver"
+ description: "Descrición"
+ delete: "Eliminar"
+ emoji: "Emoji"
+ name: "Nome"
+ save: "Gardar"
+ edit: "Editar"
+ system: "sistema"
+ url: "URL"
+ username: "Nome de usuario"
+ selection:
+ cancel: "Cancelar"
+ copy: "Copiar"
+ new_topic:
+ title: "Mover ao tema novo"
+ existing_topic:
+ title: "Mover a un tema existente"
+ new_message:
+ title: "Mover a Nova mensaxe"
+ emoji_picker:
+ objects: "Obxectos"
+ activities: "Actividades"
+ flags: "Alertas"
+ symbols: "Símbolos"
+ draft_channel_screen:
+ header: "Nova mensaxe"
+ cancel: "Cancelar"
+ notifications:
+ chat_quoted: "%{username} %{description}"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ fields:
+ message:
+ label: Mensaxe
+ review:
+ types:
+ reviewable_chat_message:
+ flagged_by: "Sinalado por"
diff --git a/plugins/chat/config/locales/client.he.yml b/plugins/chat/config/locales/client.he.yml
new file mode 100644
index 0000000000..571c4b9c90
--- /dev/null
+++ b/plugins/chat/config/locales/client.he.yml
@@ -0,0 +1,476 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+he:
+ js:
+ admin:
+ logs:
+ staff_actions:
+ actions:
+ chat_channel_status_change: "מצב ערוץ הצ׳אט השתנה"
+ chat_channel_delete: "ערוץ הצ׳אט נמחק"
+ api:
+ scopes:
+ descriptions:
+ chat:
+ create_message: "יצירת הודעת צ׳אט בערוץ מסוים."
+ about:
+ chat_messages_count: "הודעות צ׳אט"
+ chat_channels_count: "ערוצי צ׳אט"
+ chat_users_count: "משתמשי צ׳אט"
+ chat:
+ dates:
+ time_tiny: "h:mm"
+ all_loaded: "כל ההודעות מוצגות"
+ already_enabled: "כבר מופעל צ׳אט בנושא הזה. נא לרענן."
+ disabled_for_topic: "הצ׳אט מושבת בנושא הזה."
+ bot: "בוט"
+ create: "ליצור"
+ cancel: "ביטול"
+ cancel_reply: "ביטול תגובה"
+ chat_channels: "ערוצים"
+ browse_all_channels: "עיון בכל הערוצים"
+ move_to_channel:
+ title: "העברת הודעות לערוץ"
+ instructions:
+ one: "פעולה זו תעביר הודעה %{count}. יש לבחור ערוץ יעד. הודעה ממלאת מקום תיווצר בערוץ %{channelTitle} כדי לציין שההודעה הזאת הועברה."
+ two: "פעולה זו תעביר %{count} הודעות. יש לבחור ערוץ יעד. הודעה ממלאת מקום תיווצר בערוץ %{channelTitle} כדי לציין שההודעות האלו הועברו."
+ many: "פעולה זו תעביר %{count} הודעות. יש לבחור ערוץ יעד. הודעה ממלאת מקום תיווצר בערוץ %{channelTitle} כדי לציין שההודעות האלו הועברו."
+ other: "פעולה זו תעביר %{count} הודעות. יש לבחור ערוץ יעד. הודעה ממלאת מקום תיווצר בערוץ %{channelTitle} כדי לציין שההודעות האלו הועברו."
+ confirm_move: "העברת הודעות"
+ channel_settings:
+ title: "הגדרות ערוץ"
+ edit: "עריכה"
+ add: "הוספה"
+ close_channel: "סגירת ערוץ"
+ open_channel: "פתיחת ערוץ"
+ archive_channel: "העברת ערוץ לארכיון"
+ delete_channel: "מחיקת ערוץ"
+ join_channel: "הצטרפות לערוץ"
+ leave_channel: "יציאה מהערוץ"
+ join: "הצטרף"
+ leave: "עזוב"
+ channel_archive:
+ title: "העברת ערוץ לארכיון"
+ instructions: "/hooks/:key. המטען מורכב ממשתנה text (טקסט) יחיד שמוגבל ל־2000 תווים.
אנו תומכים גם במשתני text בעיצוב Slack, חילוץ קישורים ואזכורים לפי התקן https://api.slack.com/reference/surfaces/formatting, לשם כך יש להשתמש בנקודת הקצה /hooks/:key/slack."
+ selection:
+ cancel: "ביטול"
+ quote_selection: "ציטוט בנושא"
+ copy: "העתקה"
+ move_selection_to_channel: "העברה לערוץ"
+ error: "אירעה שגיאה בהעברת הודעות הצ׳אט"
+ title: "העברת צ׳אט לנושא"
+ new_topic:
+ title: "העבר לנושא חדש"
+ instructions:
+ one: "פעולה זו תיצור נושא חדש ותמלא בו את הודעת הצ׳אט שבחרת."
+ two: "פעולה זו תיצור נושא חדש ותמלא בו את %{count} הודעות הצ׳אט שבחרת."
+ many: "פעולה זו תיצור נושא חדש ותמלא בו את %{count} הודעות הצ׳אט שבחרת."
+ other: "פעולה זו תיצור נושא חדש ותמלא בו את %{count} הודעות הצ׳אט שבחרת."
+ instructions_channel_archive: "פעולה זו תיצור נושא חדש ותעביר אליו את הודעות הערוץ כארכיון."
+ existing_topic:
+ title: "העבר לנושא קיים"
+ instructions:
+ one: "נא לבחור את הנושא אליו ברצונך להעביר את הודעת הצ׳אט הזו."
+ two: "נא לבחור את הנושא אליו ברצונך להעביר את %{count} הודעות הצ׳אט האלו."
+ many: "נא לבחור את הנושא אליו ברצונך להעביר את %{count} הודעות הצ׳אט האלו."
+ other: "נא לבחור את הנושא אליו ברצונך להעביר את %{count} הודעות הצ׳אט האלו."
+ instructions_channel_archive: "נא לבחור את הנושא אליו ברצונך להעביר את הודעות הערוץ כארכיון."
+ new_message:
+ title: "העבר להודעה חדשה"
+ instructions:
+ one: "פעולה זו תיצור הודעה חדשה ותמלא בה את הודעת הצ׳אט שבחרת."
+ two: "פעולה זו תיצור הודעה חדשה ותמלא בה את %{count} הודעות הצ׳אט שבחרת."
+ many: "פעולה זו תיצור הודעה חדשה ותמלא בה את %{count} הודעות הצ׳אט שבחרת."
+ other: "פעולה זו תיצור הודעה חדשה ותמלא בה את %{count} הודעות הצ׳אט שבחרת."
+ replying_indicator:
+ single_user: "מתבצעת הקלדה מצד %{username}"
+ multiple_users: "%{commaSeparatedUsernames} וגם %{lastUsername} מקלידים"
+ many_users:
+ one: "%{commaSeparatedUsernames} ועוד %{count} בנוסף מקלידים"
+ two: "%{commaSeparatedUsernames} ועוד %{count} נוספים מקלידים"
+ many: "%{commaSeparatedUsernames} ועוד %{count} נוספים מקלידים"
+ other: "%{commaSeparatedUsernames} ועוד %{count} נוספים מקלידים"
+ retention_reminders:
+ public: "היסטוריית הערוץ נשמרת למשך %{days} ימים."
+ dm: "היסטוריית הצ׳אט האישית נשמרת למשך %{days} ימים."
+ topic_button_title: "צ׳אט"
+ flags:
+ off_topic: "הודעה זו לא תואמת לדיון הנוכחי כפי שהוגדר בכותרת הערוץ וכנראה שצריך להעביר אותה."
+ inappropriate: "הודעה זו מכילה תוכן שאדם מן השורה עשוי להחשיב כפוגעני, נצלני או מפר את הכללים המנחים את הקהילה שלנו."
+ spam: "הודעה זו היא פרסומת או השחתה. היא אינה שימושית או רלוונטית לערוץ הנוכחי."
+ notify_user: "אשמח לדבר עם אותו גורם ישירות ובאופן אישי על ההודעה שנשלחה על ידי הגורם."
+ notify_moderators: "הודעה זו דורשת את התערבות הסגל מסיבות אחרות שאינן מופיעות לעיל."
+ flagging:
+ action: "סימון הודעה בדגל"
+ emoji_picker:
+ favorites: "נפוצים"
+ smileys_&_emotion: "חייכנים ורגשות"
+ objects: "עצמים"
+ people_&_body: "אנשים וגוף"
+ travel_&_places: "טיולים ומקומות"
+ animals_&_nature: "חיות וטבע"
+ food_&_drink: "מזון ומשקאות"
+ activities: "פעילויות"
+ flags: "דגלים"
+ symbols: "סמלים"
+ search_placeholder: "חיפוש לפי שם וכינוי של האמוג׳י…"
+ no_results: "אין תוצאות"
+ draft_channel_screen:
+ header: "הודעה חדשה"
+ cancel: "ביטול"
+ notifications:
+ chat_invitation: "נשלחה אליך הזמנה להצטרף לערוץ צ׳אט"
+ chat_invitation_html: "הוזמנת להצטרף לערוץ צ׳אט על ידי %{username}"
+ chat_quoted: "%{username} %{description}"
+ popup:
+ chat_mention:
+ direct: 'אוזכרת בערוץ „%{channel}”'
+ direct_html: 'אוזכרת בערוץ „%{channel}” על ידי %{username}'
+ other_plain: 'נוסף אזכור של %{identifier} בערוץ „%{channel}”'
+ other_html: 'נוסף אזכור של %{identifier} בערוץ „%{channel}” על ידי %{username}'
+ direct_message_chat_mention:
+ direct: "אוזכרת בצ׳אט אישי"
+ direct_html: "אוזכרת בצ׳אט אישי על ידי %{username}"
+ other_plain: "נוסף אזכור של %{identifier} בצ׳אט אישי"
+ other_html: "נוסף אזכור של %{identifier} בצ׳אט אישי על ידי %{username}"
+ chat_message: "הודעת צ׳אט חדשה"
+ chat_quoted: "הודעת הצ׳אט שלך צוטטה על ידי %{username}"
+ titles:
+ chat_mention: "איזכור בצ׳אט"
+ chat_invitation: "הזמנה לצ׳אט"
+ chat_quoted: "הצ׳אט צוטט"
+ action_codes:
+ chat:
+ enabled: 'ההופעל על ידי%{who} ב־%{when}'
+ disabled: "הצ׳אט %{when} נסגר על ידי %{who}"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ title: שליחת הודעת צ׳אט
+ fields:
+ chat_channel_id:
+ label: מזהה ערוץ צ׳אט
+ message:
+ label: הודעה
+ sender:
+ label: מוען
+ description: ברירת המחדל כמו המערכת
+ review:
+ transcript:
+ view: "הצגת תמלול הודעות קודמות"
+ types:
+ reviewable_chat_message:
+ title: "הודעת צ׳אט מסומנת"
+ flagged_by: "דוגל על ידי"
+ keyboard_shortcuts_help:
+ chat:
+ title: "צ׳אט"
+ keyboard_shortcuts:
+ switch_channel_arrows: "%{shortcut} החלפת ערוץ"
+ open_quick_channel_selector: "%{shortcut} פתיחת בורר הערוצים המהיר"
+ open_insert_link_modal: "%{shortcut} הוספת קישור (עורך בלבד)"
+ composer_bold: "%{shortcut} מודגש (עורך בלבד)"
+ composer_italic: "%{shortcut} נטוי (עורך בלבד)"
+ composer_code: "%{shortcut} קוד (עורך בלבד)"
+ drawer_open: "%{shortcut} פתיחת מגירת הצ׳אט"
+ drawer_close: "%{shortcut} סגירת מגירת הצ׳אט"
+ topic_statuses:
+ chat:
+ help: "הצ׳אט מופעל בנושא הזה"
+ user:
+ allow_private_messages: "לאפשר למשתמשים אחרים לשלוח לי הודעות פרטיות והודעות ישירות בצ׳אט"
+ muted_users_instructions: "לדחות את כל ההתראות על הודעות אישיות והודעות ישירות בצ׳אט מהמשמתמשים האלה."
+ allowed_pm_users_instructions: "לאפשר רק הודעות אישיות או הודעות ישירות בצ׳אט מהמשמתמשים האלה."
+ allow_private_messages_from_specific_users: "לאפשר רק למשתמשים מסוימים לשלוח לי הודעות פרטיות או הודעות ישירות בצ׳אט"
+ ignored_users_instructions: "לדחות את כל הפוסטים, ההודעות, ההתראות, ההודעות אישיות וההודעות הישירות בצ׳אט מהמשמתמשים האלה."
+ user_menu:
+ no_chat_notifications_title: "עדיין אין לך התראות צ׳אט"
+ no_chat_notifications_body: >
+ תישלח אליך התראה בלוח הזה כאשר תישלח אליך הודעה ישירה או שיהיה @אזכור שלך בצ׳אט. תישלחנה התראות לדוא״ל שלך אם לא נכנסת מזה זמן רב.
לחיצה על הכותרת בראש כל ערוץ צ׳אט שהוא תעביר אותך להגדרת ההתראות שתישלחנה אליך באותו הערוץ. למידע נוסף, כדאי לבקר בהעדפות ההתראות שלך.
+ tabs:
+ chat_notifications: "התראות צ׳אט"
+ chat_notifications_with_unread:
+ one: "התראות צ׳אט - התראה %{count} שלא נקראה"
+ two: "התראות צ׳אט - %{count} התראות שלא נקראו"
+ many: "התראות צ׳אט - %{count} התראות שלא נקראו"
+ other: "התראות צ׳אט - %{count} התראות שלא נקראו"
diff --git a/plugins/chat/config/locales/client.hr.yml b/plugins/chat/config/locales/client.hr.yml
new file mode 100644
index 0000000000..245e81fc4e
--- /dev/null
+++ b/plugins/chat/config/locales/client.hr.yml
@@ -0,0 +1,198 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+hr:
+ js:
+ admin:
+ logs:
+ staff_actions:
+ actions:
+ chat_channel_status_change: "Status chat kanala je promijenjen"
+ chat_channel_delete: "Chat kanal je izbrisan"
+ api:
+ scopes:
+ descriptions:
+ chat:
+ create_message: "Izradite chat poruku na određenom kanalu."
+ chat:
+ dates:
+ time_tiny: "h:mm"
+ all_loaded: "Prikaz svih poruka"
+ already_enabled: "Chat je već omogućen na ovu temu. Molimo osvježite."
+ disabled_for_topic: "Chat je onemogućen na ovu temu."
+ bot: "bot"
+ create: "Kreiraj"
+ cancel: "Odustani"
+ cancel_reply: "Otkaži odgovor"
+ chat_channels: "Kanali"
+ move_to_channel:
+ title: "Premještanje poruka na kanal"
+ confirm_move: "Premjesti poruke"
+ channel_settings:
+ title: "Postavke kanala"
+ edit: "Uredi"
+ add: "Dodaj"
+ leave_channel: "Napusti kanal"
+ join: "Pridružite se"
+ leave: "Napustiti"
+ channel_archive:
+ title: "Arhiviraj kanal"
+ instructions: "/hooks/:key. Il carico utile è costituito da un solo parametro di testo, che è limitato a 2000 caratteri.
Supportiamo anche i parametri di testo con formattazione Slack limitata, l'estrazione di link e menzioni in base al formato su https://api. lack.com/reference/surfaces/formatting, ma l'endpoint /hooks/:key/slack deve essere utilizzato per questo."
+ selection:
+ cancel: "Annulla"
+ quote_selection: "Cita in argomento"
+ copy: "Copia"
+ move_selection_to_channel: "Sposta nel canale"
+ error: "Si è verificato un errore durante lo spostamento dei messaggi di chat"
+ title: "Sposta la chat nell'argomento"
+ new_topic:
+ title: "Sposta nel nuovo argomento"
+ instructions:
+ one: "Stai per creare un nuovo argomento, inserendovi il messaggio di chat che hai selezionato."
+ other: "Stai per creare un nuovo argomento, inserendovi i %{count} messaggi di chat che hai selezionato."
+ instructions_channel_archive: "Stai per creare un nuovo argomento e archiviare in esso i messaggi del canale."
+ existing_topic:
+ title: "Sposta in argomento esistente"
+ instructions:
+ one: "Scegli l'argomento in cui intendi spostare il messaggio di chat."
+ other: "Scegli l'argomento in cui intendi spostare questi %{count} messaggi di chat."
+ instructions_channel_archive: "Scegli l'argomento in cui intendi archiviare i messaggi del canale."
+ new_message:
+ title: "Sposta in un nuovo messaggio"
+ instructions:
+ one: "Stai per creare un nuovo messaggio, inserendovi il messaggio di chat che hai selezionato."
+ other: "Stai per creare un nuovo messaggio, inserendovi i %{count} messaggi di chat che hai selezionato."
+ replying_indicator:
+ single_user: "%{username} sta scrivendo"
+ multiple_users: "%{commaSeparatedUsernames} e %{lastUsername} stanno scrivendo"
+ many_users:
+ one: "%{commaSeparatedUsernames} e %{count} altro stanno scrivendo"
+ other: "%{commaSeparatedUsernames} e altri %{count} stanno scrivendo"
+ retention_reminders:
+ public: "La cronologia del canale è conservata per %{days} giorni."
+ dm: "La cronologia della chat personale è conservata per %{days} giorni."
+ topic_button_title: "Chat"
+ flags:
+ off_topic: "Questo messaggio non è rilevante per la discussione in corso in base alla definizione del titolo del canale e probabilmente dovrebbe essere spostato altrove."
+ inappropriate: "Questo messaggio ha contenuti che chiunque considererebbe offensivi o ingiuriosi, oppure contiene violazioni delle nostre linee guida della community."
+ spam: "Questo messaggio è un annuncio pubblicitario o un atto vandalico. Non è utile o rilevante per il canale corrente."
+ notify_user: "Voglio parlare con questa persona direttamente e personalmente del suo messaggio."
+ notify_moderators: "Questo messaggio richiede l'attenzione dello staff per motivi diversi da quelli sopra indicati"
+ flagging:
+ action: "Segnala messaggio"
+ emoji_picker:
+ favorites: "Utilizzati di frequente"
+ smileys_&_emotion: "Faccine ed emoticon"
+ objects: "Oggetti"
+ people_&_body: "Persone e corpo"
+ travel_&_places: "Viaggi e luoghi"
+ animals_&_nature: "Animali e natura"
+ food_&_drink: "Cibo e bevande"
+ activities: "Attività"
+ flags: "Segnalazioni"
+ symbols: "Simboli"
+ search_placeholder: "Cerca per nome emoji e alias..."
+ no_results: "Nessun risultato"
+ draft_channel_screen:
+ header: "Nuovo messaggio"
+ cancel: "Annulla"
+ notifications:
+ chat_invitation: "ti ha inviato un invito a partecipare a un canale di chat"
+ chat_invitation_html: "%{username} ti ha inviato un invito a partecipare a un canale di chat"
+ chat_quoted: "%{username} %{description}"
+ popup:
+ chat_mention:
+ direct: 'ti ha menzionato in "%{channel}"'
+ direct_html: '%{username} ti ha menzionato in "%{channel}"'
+ other_plain: 'ha menzionato %{identifier} in "%{channel}"'
+ other_html: '%{username} ha menzionato %{identifier} in "%{channel}"'
+ direct_message_chat_mention:
+ direct: "ti ha menzionato nella chat personale"
+ direct_html: "%{username} ti ha menzionato nella chat personale"
+ other_plain: "ha menzionato %{identifier} nella chat personale"
+ other_html: "%{username} ha menzionato %{identifier} nella chat personale"
+ chat_message: "Nuovo messaggio di chat"
+ chat_quoted: "%{username} ha citato il tuo messaggio di chat"
+ titles:
+ chat_mention: "Menzione in chat"
+ chat_invitation: "Invito alla chat"
+ chat_quoted: "Chat citata"
+ action_codes:
+ chat:
+ enabled: '%{who} ha abilitato la %{when}'
+ disabled: "%{who} ha chiuso la chat %{when}"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ title: Invia messaggio di chat
+ fields:
+ chat_channel_id:
+ label: ID canale chat
+ message:
+ label: Messaggio
+ sender:
+ label: Mittente
+ description: Torna ai valori predefiniti di sistema
+ review:
+ transcript:
+ view: "Visualizza la trascrizione dei messaggi precedenti"
+ types:
+ reviewable_chat_message:
+ title: "Messaggio di chat segnalato"
+ flagged_by: "Segnalato da"
+ keyboard_shortcuts_help:
+ chat:
+ title: "Chat"
+ keyboard_shortcuts:
+ switch_channel_arrows: "%{shortcut} Cambia canale"
+ open_quick_channel_selector: "%{shortcut} Apri il selettore rapido dei canali"
+ open_insert_link_modal: "%{shortcut} Inserisci collegamento ipertestuale (solo compositore)"
+ composer_bold: "%{shortcut} Grassetto (solo compositore)"
+ composer_italic: "%{shortcut} Corsivo (solo compositore)"
+ composer_code: "%{shortcut} Codice (solo compositore)"
+ drawer_open: "%{shortcut} Apri il cassetto della chat"
+ drawer_close: "%{shortcut} Chiudi il cassetto della chat"
+ topic_statuses:
+ chat:
+ help: "La chat è abilitata per questo argomento"
+ user:
+ allow_private_messages: "Consenti ad altri utenti di inviarmi messaggi personali e messaggi diretti in chat"
+ muted_users_instructions: "Elimina tutte le notifiche, i messaggi personali e i messaggi diretti della chat da questi utenti."
+ allowed_pm_users_instructions: "Consenti solo messaggi personali o messaggi diretti in chat da questi utenti."
+ allow_private_messages_from_specific_users: "Consenti solo a utenti specifici di inviarmi messaggi personali o messaggi diretti in chat"
+ ignored_users_instructions: "Elimina tutti i post, i messaggi, le notifiche, i messaggi personali e i messaggi diretti in chat di questi utenti."
+ user_menu:
+ no_chat_notifications_title: "Non hai ancora nessuna notifica di chat"
+ no_chat_notifications_body: >
+ Riceverai una notifica in questo pannello quando qualcuno ti invia un messaggio diretto o ti @menziona nella chat. Le notifiche verranno inviate anche alla tua e-mail quando non effettui l'accesso da un po' di tempo.
Fai clic sul titolo nella parte superiore di qualsiasi canale di chat per configurare quali notifiche ricevere in quel canale. Per ulteriori informazioni, consulta le tue preferenze di notifica.
+ tabs:
+ chat_notifications: "Notifiche chat"
+ chat_notifications_with_unread:
+ one: "Notifiche chat - %{count} notifica non letta"
+ other: "Notifiche chat - %{count} notifiche non lette"
diff --git a/plugins/chat/config/locales/client.ja.yml b/plugins/chat/config/locales/client.ja.yml
new file mode 100644
index 0000000000..57ba79e996
--- /dev/null
+++ b/plugins/chat/config/locales/client.ja.yml
@@ -0,0 +1,436 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+ja:
+ js:
+ admin:
+ logs:
+ staff_actions:
+ actions:
+ chat_channel_status_change: "チャットチャンネルのステータスが変更されました"
+ chat_channel_delete: "チャットチャンネルが削除されました"
+ api:
+ scopes:
+ descriptions:
+ chat:
+ create_message: "特定のチャンネルでチャットメッセージを作成します。"
+ about:
+ chat_messages_count: "チャットメッセージ"
+ chat_channels_count: "チャットチャンネル"
+ chat_users_count: "チャットユーザー"
+ chat:
+ dates:
+ time_tiny: "h:mm"
+ all_loaded: "すべてのメッセージを表示中"
+ already_enabled: "このトピックのチャットはすでに有効になっています。再読み込みしてください。"
+ disabled_for_topic: "このトピックのチャットは無効になっています。"
+ bot: "ボット"
+ create: "作成"
+ cancel: "キャンセル"
+ cancel_reply: "返信をキャンセルする"
+ chat_channels: "チャンネル"
+ browse_all_channels: "すべてのチャンネルを閲覧する"
+ move_to_channel:
+ title: "メッセージをチャンネルに移動する"
+ instructions:
+ other: "%{count} 件のメッセージを移動しようとしています。移動先のチャンネルを選択してください。%{channelTitle} チャンネルに、これらのメッセージが移動されたことを示すプレースホルダーメッセージが作成されます。"
+ confirm_move: "メッセージを移動"
+ channel_settings:
+ title: "チャンネルの設定"
+ edit: "編集"
+ add: "追加"
+ close_channel: "チャンネルを閉鎖する"
+ open_channel: "チャンネルを開く"
+ archive_channel: "チャンネルをアーカイブする"
+ delete_channel: "チャンネルを削除する"
+ join_channel: "チャンネルに参加する"
+ leave_channel: "チャンネルから退出する"
+ join: "参加"
+ leave: "退出"
+ channel_archive:
+ title: "チャンネルをアーカイブ"
+ instructions: "/hooks/:key エンドポイントを介して指定されたチャットチャンネルにボットユーザーとしてメッセージを投稿する際に使用できます。ペイロードは、2000 文字に制限された単一の text パラメーターで構成されます。
また、Slack 形式の text パラメーターも制限付きでサポートしており、https://api.slack.com/reference/surfaces/formatting のフォーマットに基いてリンクとメンションが抽出されますが、これには、/hooks/:key/slack エンドポイントを使用する必要があります。"
+ selection:
+ cancel: "キャンセル"
+ quote_selection: "トピックで引用"
+ copy: "コピー"
+ move_selection_to_channel: "チャンネルに移動"
+ error: "チャットメッセージを移動中にエラーが発生しました"
+ title: "チャットをトピックに移動"
+ new_topic:
+ title: "新しいトピックに移動"
+ instructions:
+ other: "新しいトピックを作成し、選択した %{count} 件のチャットメッセージを挿入しようとしています。"
+ instructions_channel_archive: "新しいトピックを作成し、それにチャンネルメッセージをアーカイブしようとしています。"
+ existing_topic:
+ title: "既存のトピックに移動"
+ instructions:
+ other: "それらの %{count} 件のチャットメッセージを移動するトピックを選択してください。"
+ instructions_channel_archive: "チャンネルメッセージのアーカイブ先のトピックを選択してください。"
+ new_message:
+ title: "新しいメッセージに移動"
+ instructions:
+ other: "新しいメッセージを作成し、選択した %{count} 件のチャットメッセージを挿入しようとしています。"
+ replying_indicator:
+ single_user: "%{username} が入力中です"
+ multiple_users: "%{commaSeparatedUsernames} と %{lastUsername} が入力中です"
+ many_users:
+ other: "%{commaSeparatedUsernames} と他 %{count} 人が入力中です"
+ retention_reminders:
+ public: "チャンネル履歴は %{days} 日間保持されます。"
+ dm: "パーソナルチャット履歴は %{days} 日間保持されます。"
+ topic_button_title: "チャット"
+ flags:
+ off_topic: "このメッセージは、チャンネルのタイトルで定義された現在のディスカッションとは関係がないため、おそらく別の場所に移動する必要があります。"
+ inappropriate: "このメッセージには、合理的な人が攻撃的、虐待的、またはコミュニティーガイドラインに違反すると見なすコンテンツが含まれます。"
+ spam: "このメッセージは広告または荒らし行為です。現在のチャンネルに有益な内容でなく、関連性もありません。"
+ notify_user: "この人のメッセージについて、この人に直接個人的に話を聞きたいと思います。"
+ notify_moderators: "このメッセージには、上記以外の理由でスタッフの注意が必要です。"
+ flagging:
+ action: "メッセージを通報する"
+ emoji_picker:
+ favorites: "よく使用される絵文字"
+ smileys_&_emotion: "顔文字と感情"
+ objects: "物"
+ people_&_body: "人と体"
+ travel_&_places: "旅行と場所"
+ animals_&_nature: "動物と自然"
+ food_&_drink: "食べ物と飲み物"
+ activities: "活動"
+ flags: "旗"
+ symbols: "記号"
+ search_placeholder: "絵文字名とエイリアスで検索..."
+ no_results: "結果がありません"
+ draft_channel_screen:
+ header: "新しいメッセージ"
+ cancel: "キャンセル"
+ notifications:
+ chat_invitation: "があなたをチャットチャンネルに招待しました"
+ chat_invitation_html: "%{username} があなたをチャットチャンネルに招待しました"
+ chat_quoted: "%{username} %{description}"
+ popup:
+ chat_mention:
+ direct: 'があなたを "%{channel}" でメンションしました'
+ direct_html: '%{username} があなたを "%{channel}" でメンションしました'
+ other_plain: 'が %{identifier} を "%{channel}" でメンションしました'
+ other_html: '%{username} が %{identifier} を "%{channel}" でメンションしました'
+ direct_message_chat_mention:
+ direct: "があなたをパーソナルチャットでメンションしました"
+ direct_html: "%{username} があなたをパーソナルチャットでメンションしました"
+ other_plain: "が %{identifier} をパーソナルチャットでメンションしました"
+ other_html: "%{username} が %{identifier} をパーソナルチャットでメンションしました"
+ chat_message: "新しいチャットメッセージ"
+ chat_quoted: "%{username} があなたのチャットメッセージを引用しました"
+ titles:
+ chat_mention: "チャットのメンション"
+ chat_invitation: "チャットの招待状"
+ chat_quoted: "チャットが引用されました"
+ action_codes:
+ chat:
+ enabled: '%{who} がを有効にしました: %{when}'
+ disabled: "%{who} がチャットをクローズしました: %{when}"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ title: チャットメッセージを送信する
+ fields:
+ chat_channel_id:
+ label: チャットチャンネル ID
+ message:
+ label: メッセージ
+ sender:
+ label: 送信者
+ description: デフォルトはシステムです
+ review:
+ transcript:
+ view: "前のメッセージのトランスクリプトを表示"
+ types:
+ reviewable_chat_message:
+ title: "通報されたチャットメッセージ"
+ flagged_by: "通報者"
+ keyboard_shortcuts_help:
+ chat:
+ title: "チャット"
+ keyboard_shortcuts:
+ switch_channel_arrows: "%{shortcut} チャンネルを切り替える"
+ open_quick_channel_selector: "%{shortcut} クイックチャンネルセレクターを開く"
+ open_insert_link_modal: "%{shortcut} ハイパーリンクを挿入(作成ツールのみ)"
+ composer_bold: "%{shortcut} 太字(作成ツールのみ)"
+ composer_italic: "%{shortcut} 斜体(作成ツールのみ)"
+ composer_code: "%{shortcut} コード(作成ツールのみ)"
+ drawer_open: "%{shortcut} チャットドロワーを開く"
+ drawer_close: "%{shortcut} チャットドロワーを閉じる"
+ topic_statuses:
+ chat:
+ help: "このトピックのチャットは有効になっています"
+ user:
+ allow_private_messages: "他のユーザーが個人メッセージとチャットダイレクトメッセージを自分に送信することを許可する"
+ muted_users_instructions: "これらのユーザーからのすべての通知、個人メッセージ、およびチャットダイレクトメッセージを非表示にします。"
+ allowed_pm_users_instructions: "これらのユーザーからの個人メッセージまたはチャットダイレクトメッセージのみを許可します。"
+ allow_private_messages_from_specific_users: "特定のユーザーのみが個人メッセージまたはチャットダイレクトメッセージを自分に送信することを許可する"
+ ignored_users_instructions: "これらのユーザーからのすべての投稿、メッセージ、通知、個人メッセージ、およびチャットダイレクトメッセージを非表示にします。"
+ user_menu:
+ no_chat_notifications_title: "チャット通知はまだありません"
+ no_chat_notifications_body: >
+ 誰かがあなたにダイレクトメッセージを送信したり、チャットであなたを @メンションすると、このパネルに通知されます。あなたがしばらくログインしていない場合、通知はメールでも送信されます。
チャットチャンネルの上部にあるタイトルをクリックすると、そのチャンネルでどの通知を受け取るかを構成できます。詳細については、通知設定をご覧ください。
+ tabs:
+ chat_notifications: "チャット通知"
+ chat_notifications_with_unread:
+ other: "チャット通知 - %{count} 件の未読の通知"
diff --git a/plugins/chat/config/locales/client.ko.yml b/plugins/chat/config/locales/client.ko.yml
new file mode 100644
index 0000000000..b11977d141
--- /dev/null
+++ b/plugins/chat/config/locales/client.ko.yml
@@ -0,0 +1,116 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+ko:
+ js:
+ chat:
+ create: "글"
+ cancel: "취소"
+ channel_settings:
+ edit: "편집"
+ add: "추가"
+ join: "가입"
+ leave: "나가기"
+ close: "닫기"
+ delete: "삭제하기"
+ edited: "편집됨"
+ muted: "알림끔"
+ joined: "가입"
+ email_frequency:
+ never: "알림 받지 않기"
+ flag: "신고"
+ join: "가입"
+ mention_warning:
+ dismiss: "무시"
+ reply: "댓글쓰기"
+ edit: "편집"
+ rebake_message: "HTML 다시 빌드하기"
+ bookmark_message: "북마크"
+ bookmark_message_edit: "북마크 수정"
+ save: "저장"
+ sounds:
+ none: "없음"
+ exit: "뒤로"
+ channel_status:
+ closed: "닫힘"
+ open: "열기"
+ browse:
+ back: "뒤로"
+ filter_all: 전체
+ filter_closed: 닫힘
+ chat_message_separator:
+ today: 오늘
+ yesterday: 어제
+ about_view:
+ title: 제목
+ description: 내용
+ channel_info:
+ back_to_channel: "뒤로"
+ tabs:
+ about: 소개
+ members: 회원
+ settings: 설정
+ direct_message_creator:
+ title: 새로운 메시지
+ prefix: "받는사람:"
+ create_channel:
+ type: "유형"
+ types:
+ category: "카테고리"
+ topic: "글"
+ composer:
+ italic_text: "강조하기"
+ bold_text: "굵게하기"
+ notification_levels:
+ never: "알림 받지 않기"
+ settings:
+ follow: "가입"
+ followed: "가입"
+ notifications: "알림"
+ preview: "미리 보기"
+ save: "저장"
+ saved: "저장되었습니다"
+ unfollow: "나가기"
+ incoming_webhooks:
+ back: "뒤로"
+ description: "내용"
+ delete: "삭제하기"
+ emoji: "이모티콘"
+ name: "그룹명"
+ save: "저장"
+ edit: "수정"
+ system: "시스템"
+ url: "URL"
+ username: "아이디"
+ selection:
+ cancel: "취소"
+ copy: "복사"
+ new_topic:
+ title: "새로운 주제로 이동"
+ existing_topic:
+ title: "이미 있는 주제로 옮기기"
+ new_message:
+ title: "새 메시지로 이동"
+ emoji_picker:
+ objects: "사물"
+ activities: "활동"
+ flags: "신고"
+ symbols: "기호"
+ draft_channel_screen:
+ header: "새로운 메시지"
+ cancel: "취소"
+ notifications:
+ chat_quoted: "%{username} %{description} "
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ fields:
+ message:
+ label: 메시지 보내기
+ review:
+ types:
+ reviewable_chat_message:
+ flagged_by: "신고자"
diff --git a/plugins/chat/config/locales/client.lt.yml b/plugins/chat/config/locales/client.lt.yml
new file mode 100644
index 0000000000..5a0358c584
--- /dev/null
+++ b/plugins/chat/config/locales/client.lt.yml
@@ -0,0 +1,116 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+lt:
+ js:
+ chat:
+ create: "Sukurti"
+ cancel: "Atšaukti"
+ channel_settings:
+ edit: "Redaguoti"
+ add: "Pridėti"
+ join: "Prisijungti"
+ leave: "Palikti"
+ close: "Uždaryti"
+ delete: "Pašalinti"
+ edited: "taisytas"
+ muted: "nutyldita"
+ joined: "prisijungė"
+ email_frequency:
+ never: "Niekada"
+ flag: "Pranešti"
+ join: "Prisijungti"
+ mention_warning:
+ dismiss: "praleisti"
+ reply: "Atsakyti"
+ edit: "Redaguoti"
+ rebake_message: "Perkurti HTML"
+ bookmark_message: "Žymės"
+ bookmark_message_edit: "Redaguoti žymę"
+ save: "Išsaugoti"
+ sounds:
+ none: "Nieko"
+ exit: "atgal"
+ channel_status:
+ closed: "Uždaryta"
+ open: "Atidaryti"
+ browse:
+ back: "Atgal"
+ filter_all: Visos
+ filter_closed: Uždaryta
+ chat_message_separator:
+ today: Šiandien
+ yesterday: Vakar
+ about_view:
+ title: Antraštė
+ description: Aprašymas
+ channel_info:
+ back_to_channel: "Atgal"
+ tabs:
+ about: Apie
+ members: Nariai
+ settings: Nustatymai
+ direct_message_creator:
+ title: Nauja žinutė
+ prefix: "Kam:"
+ create_channel:
+ type: "Tipas"
+ types:
+ category: "Kategorija"
+ topic: "Tema"
+ composer:
+ italic_text: "pasviras tekstas"
+ bold_text: "paryškintas tekstas"
+ notification_levels:
+ never: "Niekada"
+ settings:
+ follow: "Prisijungti"
+ followed: "Prisijungė"
+ notifications: "Pranešimai"
+ preview: "Peržiūrėti"
+ save: "Išsaugoti"
+ saved: "Išsaugota"
+ unfollow: "Palikti"
+ incoming_webhooks:
+ back: "Atgal"
+ description: "Aprašymas"
+ delete: "Pašalinti"
+ emoji: "Emoji"
+ name: "Vardas"
+ save: "Išsaugoti"
+ edit: "Redaguoti"
+ system: "sistema"
+ url: "Nuoroda"
+ username: "Vartotojo vardas"
+ selection:
+ cancel: "Atšaukti"
+ copy: "Kopijuoti"
+ new_topic:
+ title: "Perkelti į naują temą"
+ existing_topic:
+ title: "Perkelti į esamą temą"
+ new_message:
+ title: "Pereiti prie naujos žinutės"
+ emoji_picker:
+ objects: "Objektai"
+ activities: "Veikla"
+ flags: "Pažymėk!"
+ symbols: "Simboliai"
+ draft_channel_screen:
+ header: "Nauja žinutė"
+ cancel: "Atšaukti"
+ notifications:
+ chat_quoted: "%{username} %{description}"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ fields:
+ message:
+ label: Žinutės
+ review:
+ types:
+ reviewable_chat_message:
+ flagged_by: "Pažymėjo"
diff --git a/plugins/chat/config/locales/client.lv.yml b/plugins/chat/config/locales/client.lv.yml
new file mode 100644
index 0000000000..3413b0627c
--- /dev/null
+++ b/plugins/chat/config/locales/client.lv.yml
@@ -0,0 +1,112 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+lv:
+ js:
+ chat:
+ create: "Izveidot"
+ cancel: "Atcelt"
+ channel_settings:
+ edit: "Rediģēt"
+ add: "Pievienot"
+ join: "Pievienojieties"
+ leave: "Iziet"
+ close: "Aizvērt"
+ delete: "Dzēst"
+ edited: "rediģēts"
+ muted: "klusināts"
+ joined: "pievienojās"
+ email_frequency:
+ never: "Nekad"
+ flag: "Sūdzība"
+ join: "Pievienojieties"
+ mention_warning:
+ dismiss: "nerādīt"
+ reply: "Atbilde"
+ edit: "Rediģēt"
+ rebake_message: "Pārbūvēt HTML"
+ bookmark_message: "Grāmatzīmes"
+ bookmark_message_edit: "Rediģēt grāmatzīmi"
+ save: "Saglabāt"
+ sounds:
+ none: "Nav"
+ exit: "atpakaļ"
+ channel_status:
+ closed: "Slēgts"
+ open: "Atvērt"
+ browse:
+ back: "Atpakaļ"
+ filter_all: Viss
+ filter_closed: Slēgts
+ chat_message_separator:
+ today: Šodien
+ yesterday: Vakar
+ about_view:
+ title: Virsraksts
+ description: Apraksts
+ channel_info:
+ back_to_channel: "Atpakaļ"
+ tabs:
+ about: Par
+ members: Dalībnieki
+ settings: Iestatījumi
+ direct_message_creator:
+ title: Jauns ziņa
+ prefix: "Kam:"
+ create_channel:
+ type: "Tips"
+ types:
+ category: "Sadaļa"
+ topic: "Tēmas"
+ composer:
+ italic_text: "Uzsvērts teksts"
+ bold_text: "treknrakstā"
+ notification_levels:
+ never: "Nekad"
+ settings:
+ follow: "Pievienojieties"
+ followed: "Pievienojās"
+ notifications: "Paziņojumi"
+ preview: "Priekšskatījums"
+ save: "Saglabāt"
+ saved: "Saglabāts"
+ unfollow: "Iziet"
+ incoming_webhooks:
+ back: "Atpakaļ"
+ description: "Apraksts"
+ delete: "Dzēst"
+ emoji: "Smaidiņi"
+ name: "Vārds"
+ save: "Saglabāt"
+ edit: "Rediģēt"
+ system: "sistēma"
+ url: "URL"
+ username: "Lietotājvārds"
+ selection:
+ cancel: "Atcelt"
+ copy: "Kopēt"
+ new_topic:
+ title: "Pārvietot uz jaunu tēmu"
+ existing_topic:
+ title: "Pārvietot uz esošu tēmu"
+ emoji_picker:
+ objects: "Priekšmeti"
+ activities: "Aktivitātes"
+ flags: "Sūdzības"
+ symbols: "Simboli"
+ draft_channel_screen:
+ header: "Jauns ziņa"
+ cancel: "Atcelt"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ fields:
+ message:
+ label: Ziņa
+ review:
+ types:
+ reviewable_chat_message:
+ flagged_by: "Atzīmēja"
diff --git a/plugins/chat/config/locales/client.nb_NO.yml b/plugins/chat/config/locales/client.nb_NO.yml
new file mode 100644
index 0000000000..2739387de7
--- /dev/null
+++ b/plugins/chat/config/locales/client.nb_NO.yml
@@ -0,0 +1,116 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+nb_NO:
+ js:
+ chat:
+ create: "Opprett"
+ cancel: "Avbryt"
+ channel_settings:
+ edit: "Endre"
+ add: "Legg til"
+ join: "Bli medlem"
+ leave: "Forlat"
+ close: "Lukk"
+ delete: "Slett"
+ edited: "redigert"
+ muted: "ignorert"
+ joined: "medlem fra"
+ email_frequency:
+ never: "Aldri"
+ flag: "Flagg"
+ join: "Bli medlem"
+ mention_warning:
+ dismiss: "forkast"
+ reply: "Svar"
+ edit: "Endre"
+ rebake_message: "Generer HTML på nytt"
+ bookmark_message: "Bokmerke"
+ bookmark_message_edit: "Rediger bokmerke"
+ save: "Lagre"
+ sounds:
+ none: "Ingen"
+ exit: "forrige"
+ channel_status:
+ closed: "Lukket"
+ open: "Åpne"
+ browse:
+ back: "Forrige"
+ filter_all: Alle
+ filter_closed: Lukket
+ chat_message_separator:
+ today: I dag
+ yesterday: I går
+ about_view:
+ title: Tittel
+ description: Beskrivelse
+ channel_info:
+ back_to_channel: "Forrige"
+ tabs:
+ about: Om
+ members: Medlemmer
+ settings: Instillinger
+ direct_message_creator:
+ title: Ny Melding
+ prefix: "Til:"
+ create_channel:
+ type: "Type"
+ types:
+ category: "Kategori"
+ topic: "Emne"
+ composer:
+ italic_text: "kursiv tekst"
+ bold_text: "sterk tekst"
+ notification_levels:
+ never: "Aldri"
+ settings:
+ follow: "Bli medlem"
+ followed: "Medlem fra"
+ notifications: "Varsler"
+ preview: "Forhåndsvis"
+ save: "Lagre"
+ saved: "Lagret"
+ unfollow: "Forlat"
+ incoming_webhooks:
+ back: "Forrige"
+ description: "Beskrivelse"
+ delete: "Slett"
+ emoji: "Emoji"
+ name: "Navn"
+ save: "Lagre"
+ edit: "Endre"
+ system: "system"
+ url: "URL"
+ username: "Brukernavn"
+ selection:
+ cancel: "Avbryt"
+ copy: "Kopier"
+ new_topic:
+ title: "Flytt til nytt emne"
+ existing_topic:
+ title: "Flytt til eksisterende emne"
+ new_message:
+ title: "Flytt til ny melding"
+ emoji_picker:
+ objects: "Objekter"
+ activities: "Aktiviteter"
+ flags: "Flagg"
+ symbols: "Symboler"
+ draft_channel_screen:
+ header: "Ny Melding"
+ cancel: "Avbryt"
+ notifications:
+ chat_quoted: "%{username} %{description}"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ fields:
+ message:
+ label: Send
+ review:
+ types:
+ reviewable_chat_message:
+ flagged_by: "Rapportert av"
diff --git a/plugins/chat/config/locales/client.nl.yml b/plugins/chat/config/locales/client.nl.yml
new file mode 100644
index 0000000000..9fd5074bae
--- /dev/null
+++ b/plugins/chat/config/locales/client.nl.yml
@@ -0,0 +1,115 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+nl:
+ js:
+ chat:
+ create: "Aanmaken"
+ cancel: "Annuleren"
+ channel_settings:
+ edit: "Bewerken"
+ add: "Toevoegen"
+ join: "Toetreden"
+ leave: "Verlaten"
+ close: "Sluiten"
+ delete: "Verwijderen"
+ edited: "bewerkt"
+ muted: "gedempt"
+ joined: "lid sinds"
+ email_frequency:
+ never: "Nooit"
+ flag: "Markeren"
+ join: "Toetreden"
+ mention_warning:
+ dismiss: "negeren"
+ reply: "Antwoorden"
+ edit: "Bewerken"
+ rebake_message: "HTML opnieuw opbouwen"
+ bookmark_message: "Bladwijzer maken"
+ bookmark_message_edit: "Bladwijzer bewerken"
+ save: "Opslaan"
+ sounds:
+ none: "Geen"
+ exit: "vorige"
+ channel_status:
+ closed: "Gesloten"
+ open: "Openen"
+ browse:
+ filter_all: Alle
+ filter_closed: Gesloten
+ chat_message_separator:
+ today: Vandaag
+ yesterday: Gisteren
+ about_view:
+ title: Titel
+ description: Omschrijving
+ channel_info:
+ back_to_channel: "Vorige"
+ tabs:
+ about: Over
+ members: Leden
+ settings: Instellingen
+ direct_message_creator:
+ title: Nieuw bericht
+ prefix: "Aan:"
+ create_channel:
+ type: "Type"
+ types:
+ category: "Categorie"
+ topic: "Topic"
+ composer:
+ italic_text: "Cursieve tekst"
+ bold_text: "Vetgedrukte tekst"
+ notification_levels:
+ never: "Nooit"
+ settings:
+ follow: "Toetreden"
+ followed: "Lid sinds"
+ notifications: "Meldingen"
+ preview: "Voorbeeld"
+ save: "Opslaan"
+ saved: "Opgeslagen"
+ unfollow: "Verlaten"
+ incoming_webhooks:
+ back: "Vorige"
+ description: "Omschrijving"
+ delete: "Verwijderen"
+ emoji: "Emoji"
+ name: "Naam"
+ save: "Opslaan"
+ edit: "Bewerken"
+ system: "systeem"
+ url: "URL"
+ username: "Gebruikersnaam"
+ selection:
+ cancel: "Annuleren"
+ copy: "Kopiëren"
+ new_topic:
+ title: "Verplaatsen naar nieuw topic"
+ existing_topic:
+ title: "Verplaatsen naar bestaand topic"
+ new_message:
+ title: "Verplaatsen naar nieuw bericht"
+ emoji_picker:
+ objects: "Objecten"
+ activities: "Activiteiten"
+ flags: "Markeringen"
+ symbols: "Symbolen"
+ draft_channel_screen:
+ header: "Nieuw bericht"
+ cancel: "Annuleren"
+ notifications:
+ chat_quoted: "%{username} %{description}"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ fields:
+ message:
+ label: Bericht
+ review:
+ types:
+ reviewable_chat_message:
+ flagged_by: "Gemarkeerd door"
diff --git a/plugins/chat/config/locales/client.pl_PL.yml b/plugins/chat/config/locales/client.pl_PL.yml
new file mode 100644
index 0000000000..887b41a1ec
--- /dev/null
+++ b/plugins/chat/config/locales/client.pl_PL.yml
@@ -0,0 +1,332 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+pl_PL:
+ js:
+ admin:
+ logs:
+ staff_actions:
+ actions:
+ chat_channel_status_change: "Zmieniono status kanału czatu"
+ chat_channel_delete: "Kanał czatu usunięty"
+ about:
+ chat_messages_count: "Wiadomości czatu"
+ chat_channels_count: "Kanały czatu"
+ chat_users_count: "Użytkownicy czatu"
+ chat:
+ dates:
+ time_tiny: "h:mm"
+ all_loaded: "Pokazuje wszystkie wiadomości"
+ already_enabled: "Czat jest już włączony w tym temacie. Spróbuj odświeżyć strone."
+ disabled_for_topic: "Czat jest wyłączony w tym temacie."
+ bot: "bot"
+ create: "Utwórz"
+ cancel: "Anuluj"
+ cancel_reply: "Anuluj odpowiedź"
+ chat_channels: "Kanały"
+ browse_all_channels: "Przeglądaj wszystkie kanały"
+ move_to_channel:
+ confirm_move: "Przenieś wiadomości"
+ channel_settings:
+ title: "Ustawienia kanału"
+ edit: "Edytuj"
+ add: "Dodaj"
+ close_channel: "Zamknij kanał"
+ open_channel: "Otwórz kanał"
+ delete_channel: "Usuń kanał"
+ join_channel: "Dołącz do kanału"
+ leave_channel: "Opuść kanał"
+ join: "Dołącz"
+ leave: "Opuść"
+ channel_open:
+ title: "Otwórz kanał"
+ channel_close:
+ title: "Zamknij kanał"
+ channel_delete:
+ title: "Usuń kanał"
+ confirm_channel_name: "Wpisz nazwę kanału"
+ channels_list_popup:
+ browse: "Przeglądaj kanały"
+ create: "Nowy kanał"
+ click_to_join: "Kliknij tutaj, aby wyświetlić dostępne kanały."
+ close: "Zamknij"
+ collapse: "Zwiń szufladę czatu"
+ confirm_flag: "Czy na pewno chcesz oflagować wiadomość od %{username}?"
+ deleted: "Wiadomość została usunięta. [view]"
+ hidden: "Wiadomość została ukryta. [view]"
+ delete: "Usuń"
+ edited: "edytowane"
+ muted: "wyciszono"
+ joined: "dołączył"
+ empty_state:
+ direct_message: "Możesz także rozpocząć osobisty czat z jednym lub kilkoma użytkownikami."
+ title: "Nie znaleziono żadnych kanałów"
+ email_frequency:
+ never: "Nigdy"
+ title: "Powiadomienia e-mail"
+ enable: "Włącz czat"
+ flag: "Oflaguj"
+ emoji: "Wstaw emoji"
+ flagged: "Ta wiadomość została oznaczona do sprawdzenia"
+ invalid_access: "Nie masz dostępu do tego kanału czatu"
+ in_reply_to: "W odpowiedzi na"
+ heading: "Czat"
+ join: "Dołącz"
+ new_messages: "nowe wiadomości"
+ mention_warning:
+ dismiss: "odrzuć"
+ invitations_sent:
+ one: "Zaproszenie wysłane"
+ few: "Zaproszenia wysłane"
+ many: "Zaproszenia wysłane"
+ other: "Zaproszenia wysłane"
+ invite: "Zaproś do kanału"
+ without_membership:
+ one: "%{usernames} nie dołączył do tego kanału."
+ few: "%{usernames} nie dołączyli do tego kanału."
+ many: "%{usernames} nie dołączyli do tego kanału."
+ other: "%{usernames} nie dołączyli do tego kanału."
+ aria_roles:
+ header: "Nagłówek czatu"
+ channels_list: "Lista kanałów czatu"
+ no_public_channels: "Nie dołączyłeś do żadnego kanału."
+ open: "Otwórz czat"
+ open_full_page: "Otwórz czat na pełnym ekranie"
+ open_message: "Otwórz wiadomość na czacie"
+ placeholder_self: "Zanotuj coś"
+ placeholder_others: "Czat z %{messageRecipient}"
+ placeholder_new_message_disallowed: "Kanał ma %{status}, nie możesz teraz wysyłać nowych wiadomości."
+ placeholder_silenced: "W tej chwili nie możesz wysyłać wiadomości."
+ remove_upload: "Usuń plik"
+ react: "Zareaguj z emoji"
+ reply: "Odpowiedz"
+ edit: "Edytuj"
+ copy_link: "Skopiuj link"
+ rebake_message: "Odśwież HTML"
+ retry_staged_message:
+ title: "Błąd sieci"
+ action: "Wyślij ponownie?"
+ bookmark_message: "Zakładka"
+ bookmark_message_edit: "Edytuj zakładkę"
+ restore: "Przywróć usuniętą wiadomość"
+ save: "Zapisz"
+ select: "Wybierz"
+ silence: "Wycisz użytkownika"
+ return_to_list: "Powrót do listy kanałów"
+ scroll_to_bottom: "Przewiń na dół"
+ scroll_to_new_messages: "Zobacz nowe wiadomości"
+ sound:
+ title: "Dźwięk powiadomienia czatu na pulpicie"
+ sounds:
+ none: "Brak"
+ bell: "Dzwonek"
+ ding: "Ding"
+ title: "czat"
+ title_capitalized: "Czat"
+ upload: "Dołącz plik"
+ uploaded_files:
+ one: "%{count} plik"
+ few: "%{count} pliki"
+ many: "%{count} plików"
+ other: "%{count} plików"
+ you_flagged: "Oflagowałeś tę wiadomość"
+ exit: "poprzednia"
+ channel_status:
+ read_only_header: "Kanał jest tylko do odczytu"
+ read_only: "Tylko do odczytu"
+ archived_header: "Kanał został zarchiwizowany"
+ archived: "Zarchiwizowany"
+ closed_header: "Kanał jest zamknięty"
+ closed: "Zamknięta"
+ open_header: "Kanał jest otwarty"
+ open: "Otwórz"
+ browse:
+ back: "Poprzednia"
+ title: Kanały
+ filter_all: Wszystkie
+ filter_closed: Zamknięta
+ filter_archived: Zarchiwizowany
+ chat_message_separator:
+ today: Dzisiaj
+ yesterday: Wczoraj
+ members_view:
+ filter_placeholder: Znajdź członków
+ about_view:
+ associated_topic: Powiązany temat
+ title: Tytuł
+ description: Opis
+ channel_info:
+ back_to_all_channels: "Wszystkie kanały"
+ back_to_channel: "Poprzednia"
+ tabs:
+ about: O stronie
+ members: Członkowie
+ settings: Ustawienia
+ channel_edit_title_modal:
+ title: Edytuj tytuł
+ input_placeholder: Dodaj tytuł
+ channel_edit_description_modal:
+ title: Edytuj opis
+ input_placeholder: Dodaj opis
+ direct_message_creator:
+ title: Nowa wiadomość
+ prefix: "Do:"
+ no_results: Brak wyników
+ channel_selector:
+ title: "Przejdź do kanału"
+ no_channels: "Żadne kanały nie pasują do Twojego wyszukiwania"
+ channel:
+ no_memberships_found: Nie znaleziono członków
+ memberships_count:
+ one: "%{count} członek"
+ few: "%{count} członków"
+ many: "%{count} członków"
+ other: "%{count} członków"
+ create_channel:
+ choose_category:
+ label: "Wybierz kategorię"
+ none: "wybierz jeden..."
+ default_hint: Zarządzaj dostępem, odwiedzając ustawienia bezpieczeństwa %{category}
+ create: "Utwórz kanał"
+ description: "Opis (opcjonalnie)"
+ name: "Nazwa kanału"
+ title: "Nowy kanał"
+ type: "Typ"
+ types:
+ category: "Kategoria"
+ topic: "Temat"
+ reviewable:
+ type: "Wiadomość na czacie"
+ reactions:
+ only_you: "Zareagowałeś z :%{emoji}:"
+ and_others: "Ty, %{usernames} zareagowaliście z :%{emoji}:"
+ only_others: "%{usernames} zareagowali z :%{emoji}:"
+ others_and_more: "%{usernames} i %{more} inni reagowali z :%{emoji}:"
+ you_others_and_more: "Ty, %{usernames} i %{more} inni zareagowaliście z :%{emoji}:"
+ composer:
+ toggle_toolbar: "Przełącz pasek narzędzi"
+ italic_text: "wyróżniony tekst"
+ bold_text: "pogrubiony tekst"
+ code_text: "kod"
+ quote:
+ copy_success: "Cytat z czatu skopiowany do schowka"
+ notification_levels:
+ never: "Nigdy"
+ mention: "Tylko dla wzmianek"
+ always: "Dla całej aktywności"
+ settings:
+ enable_auto_join_users: "Automatycznie dodawaj wszystkich ostatnio aktywnych użytkowników"
+ disable_auto_join_users: "Zatrzymaj automatyczne dodawanie użytkowników"
+ desktop_notification_level: "Powiadomienia na pulpicie"
+ follow: "Dołącz"
+ followed: "Dołączył"
+ mobile_notification_level: "Mobilne powiadomienia push"
+ mute: "Wycisz kanał"
+ notifications: "Powiadomienia"
+ preview: "Podgląd"
+ save: "Zapisz"
+ saved: "Zapisano"
+ unfollow: "Opuść"
+ admin:
+ title: "Czat"
+ direct_messages:
+ title: "Czat osobisty"
+ new: "Nowy osobisty czat"
+ leave: "Opuść ten osobisty czat"
+ incoming_webhooks:
+ back: "Poprzednia"
+ channel_placeholder: "Wybierz kanał"
+ confirm_destroy: "Czy na pewno chcesz usunąć tego przychodzącego webhooka? Tego nie można cofnąć."
+ current_emoji: "Aktualne emoji"
+ description: "Opis"
+ delete: "Usuń"
+ emoji: "Emoji"
+ emoji_instructions: "Awatar systemowy zostanie użyty, jeśli emotikon pozostanie pusty."
+ name: "Nazwa"
+ name_placeholder: "nazwa..."
+ new: "Nowy przychodzący webhook"
+ none: "Nie utworzono żadnych istniejących przychodzących webhooków."
+ no_emoji: "Nie wybrano emotikonów"
+ post_to: "Opublikuj w"
+ reset_emoji: "Zresetuj emotikony"
+ save: "Zapisz"
+ edit: "Edytuj"
+ select_emoji: "Wybierz emoji"
+ system: "system"
+ title: "Przychodzące webhooki"
+ url: "URL"
+ username: "Nazwa użytkownika"
+ username_instructions: "Nazwa użytkownika bota, który publikuje na kanale. Domyślnie \"system\" gdy pozostanie puste."
+ selection:
+ cancel: "Anuluj"
+ copy: "Kopiuj"
+ move_selection_to_channel: "Przejdź do kanału"
+ error: "Wystąpił błąd podczas przenoszenia wiadomości czatu"
+ title: "Przenieś czat do tematu"
+ new_topic:
+ title: "Przenieś do nowego tematu"
+ instructions:
+ one: "Zamierzasz utworzyć nowy temat i wypełnić go wybraną wiadomością czatu."
+ few: "Zamierzasz utworzyć nowy temat i wypełnić go %{count} wybranymi wiadomościami czatu."
+ many: "Zamierzasz utworzyć nowy temat i wypełnić go %{count} wybranymi wiadomościami czatu."
+ other: "Zamierzasz utworzyć nowy temat i wypełnić go %{count} wybranymi wiadomościami czatu."
+ existing_topic:
+ title: "Przenieś do Istniejącego Tematu"
+ new_message:
+ title: "Przejdź do nowej wiadomości"
+ replying_indicator:
+ single_user: "%{username} pisze"
+ multiple_users: "%{commaSeparatedUsernames} i %{lastUsername} piszą"
+ retention_reminders:
+ public: "Historia kanału jest przechowywana przez %{days} dni."
+ dm: "Historia osobistego czatu jest przechowywana przez %{days} dni."
+ topic_button_title: "Czat"
+ emoji_picker:
+ smileys_&_emotion: "Buźki i emocje"
+ objects: "Obiekty"
+ people_&_body: "Ludzie i ciało"
+ travel_&_places: "Podróże i miejsca"
+ animals_&_nature: "Zwierzęta i przyroda"
+ food_&_drink: "Jedzenie i picie"
+ activities: "Aktywności"
+ flags: "Flagi"
+ symbols: "Symbolika"
+ search_placeholder: "Szukaj według nazwy emoji i aliasu..."
+ no_results: "Brak wyników"
+ draft_channel_screen:
+ header: "Nowa wiadomość"
+ cancel: "Anuluj"
+ notifications:
+ chat_quoted: "%{username} %{description}"
+ popup:
+ chat_mention:
+ direct: 'wspomniał o Tobie w "%{channel}"'
+ chat_message: "Nowa wiadomość na czacie"
+ chat_quoted: "%{username} zacytował Twoją wiadomość na czacie"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ title: Wyślij wiadomość na czacie
+ fields:
+ chat_channel_id:
+ label: ID kanału czatu
+ message:
+ label: Wiadomość
+ sender:
+ label: Nadawca
+ review:
+ types:
+ reviewable_chat_message:
+ flagged_by: "Oflagowany przez"
+ keyboard_shortcuts_help:
+ chat:
+ title: "Czat"
+ keyboard_shortcuts:
+ switch_channel_arrows: "%{shortcut} Przełącz kanał"
+ open_quick_channel_selector: "%{shortcut} Otwórz szybki selektor kanałów"
+ user_menu:
+ tabs:
+ chat_notifications: "Powiadomienia czatu"
diff --git a/plugins/chat/config/locales/client.pt.yml b/plugins/chat/config/locales/client.pt.yml
new file mode 100644
index 0000000000..c7d4d8b87a
--- /dev/null
+++ b/plugins/chat/config/locales/client.pt.yml
@@ -0,0 +1,116 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+pt:
+ js:
+ chat:
+ create: "Criar"
+ cancel: "Cancelar"
+ channel_settings:
+ edit: "Editar"
+ add: "Adicionar"
+ join: "Entrar"
+ leave: "Sair"
+ close: "Fechar"
+ delete: "Eliminar"
+ edited: "editado"
+ muted: "silenciado"
+ joined: "juntou-se"
+ email_frequency:
+ never: "Nunca"
+ flag: "Denunciar"
+ join: "Entrar"
+ mention_warning:
+ dismiss: "marcar Visto"
+ reply: "Responder"
+ edit: "Editar"
+ rebake_message: "Reconstruir HTML"
+ bookmark_message: "Adicionar Marcador"
+ bookmark_message_edit: "Editar Favorito"
+ save: "Guardar"
+ sounds:
+ none: "Nenhuma"
+ exit: "retroceder"
+ channel_status:
+ closed: "Fechado"
+ open: "Abrir"
+ browse:
+ back: "Retroceder"
+ filter_all: Tudo
+ filter_closed: Fechado
+ chat_message_separator:
+ today: Hoje
+ yesterday: Ontem
+ about_view:
+ title: Título
+ description: Descrição
+ channel_info:
+ back_to_channel: "Retroceder"
+ tabs:
+ about: Sobre
+ members: Membros
+ settings: Configurações
+ direct_message_creator:
+ title: Nova Mensagem
+ prefix: "Para:"
+ create_channel:
+ type: "Tipo"
+ types:
+ category: "Categoria"
+ topic: "Tópico"
+ composer:
+ italic_text: "texto em itálico"
+ bold_text: "texto em negrito"
+ notification_levels:
+ never: "Nunca"
+ settings:
+ follow: "Entrar"
+ followed: "Juntou-se"
+ notifications: "Notificações"
+ preview: "Pré-visualização"
+ save: "Guardar"
+ saved: "Guardado"
+ unfollow: "Sair"
+ incoming_webhooks:
+ back: "Retroceder"
+ description: "Descrição"
+ delete: "Eliminar"
+ emoji: "Emoji"
+ name: "Nome"
+ save: "Guardar"
+ edit: "Editar"
+ system: "sistema"
+ url: "URL"
+ username: "Nome de Utilizador"
+ selection:
+ cancel: "Cancelar"
+ copy: "Copiar"
+ new_topic:
+ title: "Mover para um Novo Tópico"
+ existing_topic:
+ title: "Mover para Tópico Existente"
+ new_message:
+ title: "Mover para Nova Mensagem"
+ emoji_picker:
+ objects: "Objetos"
+ activities: "Atividades"
+ flags: "Sinalizações"
+ symbols: "Símbolos"
+ draft_channel_screen:
+ header: "Nova Mensagem"
+ cancel: "Cancelar"
+ notifications:
+ chat_quoted: "%{username} %{description}"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ fields:
+ message:
+ label: Mensagem
+ review:
+ types:
+ reviewable_chat_message:
+ flagged_by: "Sinalizado por"
diff --git a/plugins/chat/config/locales/client.pt_BR.yml b/plugins/chat/config/locales/client.pt_BR.yml
new file mode 100644
index 0000000000..fc841e3308
--- /dev/null
+++ b/plugins/chat/config/locales/client.pt_BR.yml
@@ -0,0 +1,449 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+pt_BR:
+ js:
+ admin:
+ logs:
+ staff_actions:
+ actions:
+ chat_channel_status_change: "Status do canal de chat alterado"
+ chat_channel_delete: "Canal de chat excluído"
+ api:
+ scopes:
+ descriptions:
+ chat:
+ create_message: "Crie uma mensagem de chat em um canal especificado."
+ about:
+ chat_messages_count: "Mensagens de chat"
+ chat_channels_count: "Canais de chat"
+ chat_users_count: "Usuários do chat"
+ chat:
+ dates:
+ time_tiny: "h:mm"
+ all_loaded: "Mostrando todas as mensagens"
+ already_enabled: "O chat já foi ativado neste tópico. Atualize."
+ disabled_for_topic: "O chat está desativado neste tópico."
+ bot: "robô"
+ create: "Criar"
+ cancel: "Cancelar"
+ cancel_reply: "Cancelar resposta"
+ chat_channels: "Canais"
+ browse_all_channels: "Navegar por todos os canais"
+ move_to_channel:
+ title: "Mover mensagens para o canal"
+ instructions:
+ one: "Você está movendo %{count} mensagem. Selecione um canal de destino. Uma mensagem de espaço reservado será criada no canal %{channelTitle} para indicar que esta mensagem foi movida."
+ other: "Você está movendo %{count} mensagens. Selecione um canal de destino. Uma mensagem de espaço reservado será criada no canal %{channelTitle} para indicar que essas mensagens foram movidas."
+ confirm_move: "Mover mensagens"
+ channel_settings:
+ title: "Definições do canal"
+ edit: "Editar"
+ add: "Adicionar"
+ close_channel: "Fechar canal"
+ open_channel: "Abrir canal"
+ archive_channel: "Arquivar canal"
+ delete_channel: "Excluir canal"
+ join_channel: "Entrar no canal"
+ leave_channel: "Sair do canal"
+ join: "Participar"
+ leave: "Sair"
+ channel_archive:
+ title: "Arquivar canal"
+ instructions: "/hooks/:key . A carga útil consiste em um único parâmetro text , limitado a 2.000 caracteres.
Também oferecemos suporte limitado a parâmetros text formatados pelo Slack, extraindo links e menções com base no formato em https://api.slack.com/reference/surfaces/formatting, mas /hooks/:key/ O endpoint do slack deve ser usado para isso."
+ selection:
+ cancel: "Cancelar"
+ quote_selection: "Citar no tópico"
+ copy: "Copiar"
+ move_selection_to_channel: "Mover para o canal"
+ error: "Houve um erro ao mover as mensagens de chat"
+ title: "Mover chat para tópico"
+ new_topic:
+ title: "Mover para novo tópico"
+ instructions:
+ one: "Você está prestes a criar um novo tópico e preenchê-lo com a mensagem de chat selecionada."
+ other: "Você está prestes a criar um novo tópico e preenchê-lo com as %{count} mensagens de chat selecionadas."
+ instructions_channel_archive: "Você está prestes a criar um novo tópico e arquivar as mensagens do canal nele."
+ existing_topic:
+ title: "Mover para tópico existente"
+ instructions:
+ one: "Escolha o tópico para o qual você gostaria de mover a mensagem de chat."
+ other: "Escolha o tópico para o qual você gostaria de mover as %{count} mensagens de chat."
+ instructions_channel_archive: "Escolha o tópico para o qual você gostaria de arquivar as mensagens de canal."
+ new_message:
+ title: "Mover para nova mensagem"
+ instructions:
+ one: "Você está prestes a criar uma nova mensagem e preenchê-la com a mensagem de chat selecionada."
+ other: "Você está prestes a criar uma nova mensagem e preenchê-la com as %{count} mensagens de chat selecionadas."
+ replying_indicator:
+ single_user: "%{username} está digitando"
+ multiple_users: "%{commaSeparatedUsernames} e %{lastUsername} estão digitando"
+ many_users:
+ one: "%{commaSeparatedUsernames} e mais %{count} estão digitando"
+ other: "%{commaSeparatedUsernames} e mais %{count} estão digitando"
+ retention_reminders:
+ public: "Histórico do canal é mantido por %{days} dias."
+ dm: "Histórico de conversas pessoais é mantido por %{days} dias."
+ topic_button_title: "Chat"
+ flags:
+ off_topic: "Esta mensagem não é relevante para a discussão atual, conforme definido pelo título do canal, e provavelmente deve ser movida para outro lugar."
+ inappropriate: "Esta mensagem contém conteúdo que uma pessoa razoável consideraria ofensivo, abusivo ou uma violação de nossas diretrizes da comunidade."
+ spam: "Esta mensagem é uma propaganda, ou vandalismo. Não é útil ou relevante para o canal atual."
+ notify_user: "Quero falar com essa pessoa diretamente sobre sua mensagem."
+ notify_moderators: "Esta mensagem requer atenção da equipe por outro motivo não listado acima."
+ flagging:
+ action: "Sinalizar mensagem"
+ emoji_picker:
+ favorites: "Usado frequentemente"
+ smileys_&_emotion: "Smileys e emoções"
+ objects: "Objetos"
+ people_&_body: "Pessoas e corpo"
+ travel_&_places: "Viagens e lugares"
+ animals_&_nature: "Animais e natureza"
+ food_&_drink: "Comida e bebida"
+ activities: "Atividades"
+ flags: "Sinalizações"
+ symbols: "Símbolos"
+ search_placeholder: "Pesquisar por nome de emoji e codinomes..."
+ no_results: "Nenhum resultado"
+ draft_channel_screen:
+ header: "Novas mensagens"
+ cancel: "Cancelar"
+ notifications:
+ chat_invitation: "convidou você para participar de um canal de chat"
+ chat_invitation_html: "%{username} convidou você para entrar em um canal de chat"
+ chat_quoted: "%{username} %{description}"
+ popup:
+ chat_mention:
+ direct: 'mencionou você em "%{channel}"'
+ direct_html: '%{username} convidou você em "%{channel}"'
+ other_plain: 'mencionou %{identifier} em "%{channel}"'
+ other_html: '%{username} mencionou %{identifier} em "%{channel}"'
+ direct_message_chat_mention:
+ direct: "mencionei você no chat pessoal"
+ direct_html: "%{username} mencionou você no chat pessoal"
+ other_plain: "mencionou %{identifier} no chat pessoal"
+ other_html: "%{username} mencionou %{identifier} no chat pessoal"
+ chat_message: "Nova mensagem de chat"
+ chat_quoted: "%{username} citou sua mensagem do chat"
+ titles:
+ chat_mention: "Menção no chat"
+ chat_invitation: "Convite para chat"
+ chat_quoted: "Chat citado"
+ action_codes:
+ chat:
+ enabled: '%{who} ativou o %{when}'
+ disabled: "%{who} fechou o chat %{when}"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ title: Enviar mensagem de chat
+ fields:
+ chat_channel_id:
+ label: ID do canal de chat
+ message:
+ label: Mensagem
+ sender:
+ label: Remetente
+ description: Padrões do sistema
+ review:
+ transcript:
+ view: "Ver transcrição de mensagens anteriores"
+ types:
+ reviewable_chat_message:
+ title: "Mensagem de chat sinalizada"
+ flagged_by: "Sinalizada por"
+ keyboard_shortcuts_help:
+ chat:
+ title: "Chat"
+ keyboard_shortcuts:
+ switch_channel_arrows: "%{shortcut} Mudar de canal"
+ open_quick_channel_selector: "%{shortcut} Abrir o seletor rápido de canais"
+ open_insert_link_modal: "%{shortcut} Inserir hiperlink (somente compositor)"
+ composer_bold: "%{shortcut} Negrito (somente compositor)"
+ composer_italic: "%{shortcut} Itálico (somente compositor)"
+ composer_code: "%{shortcut} Código (somente compositor)"
+ drawer_open: "%{shortcut} Abrir a gaveta do chat"
+ drawer_close: "%{shortcut} Fechar a gaveta do chat"
+ topic_statuses:
+ chat:
+ help: "O chat está sativado para este tópico"
+ user:
+ allow_private_messages: "Permitir que outros usuários me enviem mensagens pessoais e mensagens diretas no chat"
+ muted_users_instructions: "Suprimir todas as notificações, mensagens pessoais e mensagens diretas no chat desses usuários."
+ allowed_pm_users_instructions: "Permitir apenas mensagens pessoais ou mensagens diretas no chat desses usuários."
+ allow_private_messages_from_specific_users: "Permitir apenas que usuários específicos me enviem mensagens pessoais ou mensagens diretas no chat"
+ ignored_users_instructions: "Suprimir todas as postagens, mensagens, notificações, mensagens pessoais e mensagens diretas no chat desses usuários."
+ user_menu:
+ no_chat_notifications_title: "Você ainda não tem notificações no chat"
+ no_chat_notifications_body: >
+ Você receberá uma notificação neste painel quando alguém enviar uma mensagem ou @mencionar você no chat. As notificações também serão enviadas para o seu e-mail quando você não fizer login por um tempo.
Clique no título na parte superior de qualquer canal de chat para configurar quais notificações você recebe nesse canal. Para saber mais, consulte suas preferências de notificação.
+ tabs:
+ chat_notifications: "Notificações do chat"
+ chat_notifications_with_unread:
+ one: "Notificações do chat - %{count} notificação não lida"
+ other: "Notificações do chat - %{count} notificações não lidas"
diff --git a/plugins/chat/config/locales/client.ro.yml b/plugins/chat/config/locales/client.ro.yml
new file mode 100644
index 0000000000..631792d30c
--- /dev/null
+++ b/plugins/chat/config/locales/client.ro.yml
@@ -0,0 +1,111 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+ro:
+ js:
+ chat:
+ create: "Creează"
+ cancel: "Anulare"
+ channel_settings:
+ edit: "Modifică"
+ add: "Adaugă"
+ join: "Alătură-te"
+ leave: "Părăsește"
+ close: "Închide sondajul"
+ delete: "Șterge"
+ muted: "silențios"
+ joined: "înscris"
+ email_frequency:
+ never: "Niciodată"
+ flag: "Marchează cu marcaj de avertizare"
+ join: "Alătură-te"
+ mention_warning:
+ dismiss: "renunță"
+ reply: "Răspunde"
+ edit: "Modifică"
+ rebake_message: "Reconstruieşte HTML"
+ bookmark_message: "Semn de carte"
+ bookmark_message_edit: "Editați marcajul"
+ save: "Salvare"
+ sounds:
+ none: "Nimeni"
+ exit: "înapoi"
+ channel_status:
+ closed: "Închis"
+ open: "Deschide sondajul"
+ browse:
+ back: "Înapoi"
+ filter_all: Toate
+ filter_closed: Închis
+ chat_message_separator:
+ today: Astăzi
+ yesterday: Ieri
+ about_view:
+ title: Titlu
+ description: Descriere
+ channel_info:
+ back_to_channel: "Înapoi"
+ tabs:
+ about: Despre
+ members: Membrii
+ settings: Opțiuni
+ direct_message_creator:
+ title: Mesaj nou
+ prefix: "Către:"
+ create_channel:
+ type: "Tip"
+ types:
+ category: "Categorie"
+ topic: "Discuție"
+ composer:
+ italic_text: "text italic"
+ bold_text: "text aldin"
+ notification_levels:
+ never: "Niciodată"
+ settings:
+ follow: "Alătură-te"
+ followed: "Înscris"
+ notifications: "Notificări"
+ preview: "Previzualizează"
+ save: "Salvare"
+ saved: "Salvat"
+ unfollow: "Părăsește"
+ incoming_webhooks:
+ back: "Înapoi"
+ description: "Descriere"
+ delete: "Șterge"
+ emoji: "Emoji"
+ name: "Nume"
+ save: "Salvare"
+ edit: "Modifică"
+ system: "sistem"
+ url: "URL"
+ username: "Nume utilizator"
+ selection:
+ cancel: "Anulare"
+ copy: "Copiază"
+ new_topic:
+ title: "Mutare în subiect nou."
+ existing_topic:
+ title: "Mută în subiect deja existent"
+ emoji_picker:
+ objects: "Obiecte"
+ flags: "Marcaje de avertizare"
+ draft_channel_screen:
+ header: "Mesaj nou"
+ cancel: "Anulare"
+ notifications:
+ chat_quoted: "%{username} %{description}"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ fields:
+ message:
+ label: Mesaj
+ review:
+ types:
+ reviewable_chat_message:
+ flagged_by: "Semnalat de"
diff --git a/plugins/chat/config/locales/client.ru.yml b/plugins/chat/config/locales/client.ru.yml
new file mode 100644
index 0000000000..1e48e9caa9
--- /dev/null
+++ b/plugins/chat/config/locales/client.ru.yml
@@ -0,0 +1,474 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+ru:
+ js:
+ admin:
+ logs:
+ staff_actions:
+ actions:
+ chat_channel_status_change: "Изменение статуса канала чата"
+ chat_channel_delete: "Удаление канала чата"
+ api:
+ scopes:
+ descriptions:
+ chat:
+ create_message: "Создать сообщение чата в указанном канале."
+ about:
+ chat_messages_count: "Сообщения чата"
+ chat_channels_count: "Каналы чата"
+ chat_users_count: "Пользователи чата"
+ chat:
+ dates:
+ time_tiny: "ч:мм"
+ all_loaded: "Отобразить все сообщения"
+ already_enabled: "Чат в этой теме уже включён . Пожалуйста, обновите страницу."
+ disabled_for_topic: "Чат в этой теме отключён."
+ bot: "Бот"
+ create: "Создать"
+ cancel: "Отмена"
+ cancel_reply: "Отменить ответ"
+ chat_channels: "Каналы"
+ browse_all_channels: "Просмотреть все каналы"
+ move_to_channel:
+ title: "Переместить сообщения в канал"
+ instructions:
+ one: "Вы перемещаете %{count} сообщение. Выберите целевой канал. В канале %{channelTitle} будет создано сообщение, указывающее, что это сообщение было перемещено."
+ few: "Вы перемещаете %{count} сообщения. Выберите целевой канал. В канале %{channelTitle} будет создано сообщение, указывающее, что эти сообщения были перемещены."
+ many: "Вы перемещаете %{count} сообщений. Выберите целевой канал. В канале %{channelTitle} будет создано сообщение, указывающее, что эти сообщения были перемещены."
+ other: "Вы перемещаете %{count} сообщений. Выберите целевой канал. В канале %{channelTitle} будет создано сообщение, указывающее, что эти сообщения были перемещены."
+ confirm_move: "Переместить сообщения"
+ channel_settings:
+ title: "Настройки канала"
+ edit: "Изменить"
+ add: "Добавить"
+ close_channel: "Закрыть канал"
+ open_channel: "Открыть канал"
+ archive_channel: "Архивировать канал"
+ delete_channel: "Удалить канал"
+ join_channel: "Подписаться на канал"
+ leave_channel: "Покинуть канал"
+ join: "Подписаться"
+ leave: "Отписаться"
+ channel_archive:
+ title: "Архивировать канал"
+ instructions: "/hooks/:key. Полезная нагрузка состоит из одного параметра text, который ограничен 2000 символами.
Мы также поддерживаем ограниченные Slack-форматированные текстовые параметры, извлекая ссылки и упоминания на основе формата https://api.slack.com/reference/surfaces/formatting, но для этого необходимо использовать конечную точку /hooks/:key/slack."
+ selection:
+ cancel: "Отмена"
+ quote_selection: "Цитировать в теме"
+ copy: "Копировать"
+ move_selection_to_channel: "Переместить в канал"
+ error: "При перемещении сообщений чата произошла ошибка"
+ title: "Переместить чат в тему"
+ new_topic:
+ title: "Переместить в новую тему"
+ instructions:
+ one: "Вы собираетесь создать новую тему и заполнить её сообщением, которое вы выбрали."
+ few: "Вы собираетесь создать новую тему и заполнить её %{count} сообщениями, которые вы выбрали."
+ many: "Вы собираетесь создать новую тему и заполнить её %{count} сообщениями, которые вы выбрали."
+ other: "Вы собираетесь создать новую тему и заполнить её %{count} сообщениями, которые вы выбрали."
+ instructions_channel_archive: "Вы собираетесь создать новую тему и заархивировать в неё сообщения канала."
+ existing_topic:
+ title: "Переместить в существующую тему"
+ instructions:
+ one: "Пожалуйста, выберите тему, в которую вы хотите переместить это сообщение."
+ few: "Пожалуйста, выберите тему, в которую вы хотите переместить эти %{count} сообщения."
+ many: "Пожалуйста, выберите тему, в которую вы хотите переместить эти %{count} сообщений."
+ other: "Пожалуйста, выберите тему, в которую вы хотите переместить эти %{count} сообщений."
+ instructions_channel_archive: "Пожалуйста, выберите тему, в которую вы хотите заархивировать сообщения канала."
+ new_message:
+ title: "Переместить в новое сообщение"
+ instructions:
+ one: "Вы собираетесь создать новое сообщение и заполнить её сообщением чата, которое вы выбрали."
+ few: "Вы собираетесь создать новое сообщение и заполнить её %{count} сообщениями чата, которые вы выбрали."
+ many: "Вы собираетесь создать новое сообщение и заполнить её %{count} сообщениями чата, которые вы выбрали."
+ other: "Вы собираетесь создать новое сообщение и заполнить её %{count} сообщениями чата, которые вы выбрали."
+ replying_indicator:
+ single_user: "%{username} печатает"
+ multiple_users: "%{commaSeparatedUsernames} и %{lastUsername} печатают"
+ many_users:
+ one: "Отвечают %{commaSeparatedUsernames} и ещё %{count} пользователь"
+ few: "Отвечают %{commaSeparatedUsernames} и ещё %{count} пользователя"
+ many: "Отвечают %{commaSeparatedUsernames} и ещё %{count} пользователей"
+ other: "Отвечают %{commaSeparatedUsernames} и ещё %{count} пользователей"
+ retention_reminders:
+ public: "История канала хранится %{days} дней."
+ dm: "История личного чата хранится %{days} дней."
+ topic_button_title: "Чат"
+ flags:
+ off_topic: "Это сообщение не имеет отношения к текущему обсуждению, как указано в названии канала, и, вероятно, его следует переместить в другое место."
+ inappropriate: "Это сообщение содержит контент, который разумный человек счёл бы недопустимым, оскорбительным или нарушающим основные принципы нашего сообщества."
+ spam: "Это сообщение является рекламой. Оно не несёт полезной нагрузки или не имеет отношения к текущему каналу."
+ notify_user: "Я хочу поговорить с этим человеком напрямую и обсудить его сообщение."
+ notify_moderators: "Это сообщение требует внимания модератора по причине, не указанной выше."
+ flagging:
+ action: "Пожаловаться на сообщение"
+ emoji_picker:
+ favorites: "Часто используемые"
+ smileys_&_emotion: "Смайлики и эмоции"
+ objects: "Объекты"
+ people_&_body: "Люди и части тел"
+ travel_&_places: "Путешествия и места"
+ animals_&_nature: "Животные и природа"
+ activities: "Деятельность"
+ flags: "Флаги"
+ symbols: "Символы"
+ search_placeholder: "Поиск по названию эмодзи и псевдониму..."
+ no_results: "Нет результатов"
+ draft_channel_screen:
+ header: "Новое сообщение"
+ cancel: "Отмена"
+ notifications:
+ chat_invitation: "пригласил вас присоединиться к каналу чата"
+ chat_invitation_html: "Пользователь %{username} пригласил вас присоединиться к каналу"
+ chat_quoted: "%{username} %{description}"
+ popup:
+ chat_mention:
+ direct: 'упомянул вас в "%{channel}"'
+ direct_html: 'Пользователь %{username} упомянул вас на канале "%{channel}"'
+ other_plain: 'упомянул %{identifier} в канале "%{channel}"'
+ other_html: 'Пользователь %{username} упомянул %{identifier} на канале "%{channel}"'
+ direct_message_chat_mention:
+ direct: "упомянул вас в личном чате"
+ direct_html: "Пользователь %{username} упомянул вас в личном чате"
+ other_plain: "упомянул %{identifier} в личном чате"
+ other_html: "Пользователь %{username} упомянул @%{identifier} в личном чате"
+ chat_message: "Новое сообщение в чате"
+ chat_quoted: "Пользователь %{username} процитировал ваше сообщение в чате"
+ titles:
+ chat_mention: "Упоминание в чате"
+ chat_invitation: "Приглашение в чат"
+ chat_quoted: "Цитирование чата"
+ action_codes:
+ chat:
+ enabled: '%{who} включил %{when}'
+ disabled: "%{who} закрыл чат %{when}"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ title: Отправить сообщение в чат
+ fields:
+ chat_channel_id:
+ label: ID канала чата
+ message:
+ label: Сообщение
+ sender:
+ label: Отправитель
+ description: Системные значения по умолчанию
+ review:
+ transcript:
+ view: "Просмотр предыдущих сообщений"
+ types:
+ reviewable_chat_message:
+ title: "Сообщение на премодерации"
+ flagged_by: "Жалоба от"
+ keyboard_shortcuts_help:
+ chat:
+ title: "Чат"
+ keyboard_shortcuts:
+ switch_channel_arrows: "%{shortcut} Переключить канал"
+ open_quick_channel_selector: "%{shortcut} Открыть переключатель каналов"
+ open_insert_link_modal: "%{shortcut} Вставить гиперссылку (только в редакторе)"
+ composer_bold: "%{shortcut} Жирный (только в редакторе)"
+ composer_italic: "%{shortcut} Курсив (только в редакторе)"
+ composer_code: "%{shortcut} Код (только в редакторе)"
+ drawer_open: "%{shortcut} Открыть панель чата"
+ drawer_close: "%{shortcut} Закрыть панель чата"
+ topic_statuses:
+ chat:
+ help: "Чат включён для этой темы"
+ user:
+ allow_private_messages: "Разрешить другим пользователям отправлять мне личные сообщения и прямые сообщения в чате"
+ muted_users_instructions: "Не показывать уведомления, личные сообщения и прямые сообщения в чате от этих пользователей."
+ allowed_pm_users_instructions: "Разрешить только личные сообщения или прямые сообщения в чате от этих пользователей."
+ allow_private_messages_from_specific_users: "Разрешить только определённым пользователям отправлять мне личные сообщения или прямые сообщения в чате"
+ ignored_users_instructions: "Не показывать сообщения, личные сообщения, уведомления, прямые и личные сообщения чата от этих пользователей."
+ user_menu:
+ no_chat_notifications_title: "У вас пока нет уведомлений чата"
+ no_chat_notifications_body: >
+ На этой панели появится уведомление, когда кто-то напишет вам напрямую или @упомянет вас в чате. Уведомления также будут отправлены на вашу электронную почту, если вы отсутствовали на форуме в течение некоторого времени.
Кликните заголовок в верхней части любого канала чата, чтобы настроить уведомления, которые вы будете получать в этом канале. Для получения дополнительной информации см. настройки уведомлений.
+ tabs:
+ chat_notifications: "Уведомления чата"
+ chat_notifications_with_unread:
+ one: "Уведомления чата - %{count} непрочитанное уведомление"
+ few: "Уведомления чата - %{count} непрочитанных уведомления"
+ many: "Уведомления чата - %{count} непрочитанных уведомлений"
+ other: "Уведомления чата - %{count} непрочитанных уведомлений"
diff --git a/plugins/chat/config/locales/client.sk.yml b/plugins/chat/config/locales/client.sk.yml
new file mode 100644
index 0000000000..8334d004da
--- /dev/null
+++ b/plugins/chat/config/locales/client.sk.yml
@@ -0,0 +1,104 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+sk:
+ js:
+ chat:
+ create: "Vytvoriť"
+ cancel: "Zrušiť"
+ channel_settings:
+ edit: "Upraviť"
+ add: "Pridať"
+ join: "Pridať sa"
+ leave: "Opustiť"
+ close: "Zavrieť"
+ delete: "Odstrániť"
+ muted: "ignorovaní"
+ joined: "vytvorený"
+ email_frequency:
+ never: "Nikdy"
+ flag: "Označenie"
+ join: "Pridať sa"
+ mention_warning:
+ dismiss: "zahodiť"
+ reply: "Odpoveď"
+ edit: "Upraviť"
+ rebake_message: "Pregenerovať HTML"
+ bookmark_message: "Záložka"
+ save: "Uložiť"
+ sounds:
+ none: "Žiadny"
+ exit: "späť"
+ channel_status:
+ closed: "Zatvorené"
+ open: "Zahájiť"
+ browse:
+ back: "Späť"
+ filter_all: Všetky
+ filter_closed: Zatvorené
+ chat_message_separator:
+ today: Dnes
+ yesterday: Včera
+ about_view:
+ title: Názov
+ description: Popis
+ channel_info:
+ back_to_channel: "Späť"
+ tabs:
+ about: O stránke
+ members: Členovia
+ settings: Nastavenia
+ direct_message_creator:
+ title: Nová správa
+ prefix: "Komu:"
+ create_channel:
+ type: "Typ"
+ types:
+ category: "Kategória"
+ topic: "Témy"
+ composer:
+ italic_text: "zdôraznený text"
+ bold_text: "výrazný text"
+ notification_levels:
+ never: "Nikdy"
+ settings:
+ follow: "Pridať sa"
+ followed: "Vytvorený"
+ notifications: "Upozornenia"
+ preview: "Ukážka"
+ save: "Uložiť"
+ saved: "Uložené"
+ unfollow: "Opustiť"
+ incoming_webhooks:
+ back: "Späť"
+ description: "Popis"
+ delete: "Odstrániť"
+ emoji: "Emoji"
+ name: "Meno"
+ save: "Uložiť"
+ edit: "Upraviť"
+ system: "systém"
+ url: "URL"
+ username: "Používateľské meno"
+ selection:
+ cancel: "Zrušiť"
+ copy: "Kopírovať"
+ new_topic:
+ title: "Presuň na novú tému"
+ existing_topic:
+ title: "Presuň do existujúcej témy."
+ emoji_picker:
+ objects: "Objekty"
+ flags: "Označenia"
+ draft_channel_screen:
+ header: "Nová správa"
+ cancel: "Zrušiť"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ fields:
+ message:
+ label: Správa
diff --git a/plugins/chat/config/locales/client.sl.yml b/plugins/chat/config/locales/client.sl.yml
new file mode 100644
index 0000000000..1f9ced9dba
--- /dev/null
+++ b/plugins/chat/config/locales/client.sl.yml
@@ -0,0 +1,115 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+sl:
+ js:
+ chat:
+ create: "Ustvari"
+ cancel: "Prekliči"
+ channel_settings:
+ edit: "Uredi"
+ add: "Dodaj"
+ join: "Pridruži se"
+ leave: "Zapusti"
+ close: "Zapri"
+ delete: "Izbriši"
+ edited: "urejen"
+ muted: "utišani"
+ joined: "pridružen"
+ email_frequency:
+ never: "Nikoli"
+ flag: "Prijavi"
+ join: "Pridruži se"
+ mention_warning:
+ dismiss: "opusti"
+ reply: "Odgovori"
+ edit: "Uredi"
+ rebake_message: "Obnovi HTML"
+ bookmark_message: "Zaznamek"
+ save: "Shrani"
+ sounds:
+ none: "Brez"
+ exit: "nazaj"
+ channel_status:
+ closed: "Zaprto"
+ open: "Odpri"
+ browse:
+ back: "Nazaj"
+ filter_all: Vse
+ filter_closed: Zaprto
+ chat_message_separator:
+ today: Danes
+ yesterday: Včeraj
+ about_view:
+ title: Naziv
+ description: Opis
+ channel_info:
+ back_to_channel: "Nazaj"
+ tabs:
+ about: O nas
+ members: Člani
+ settings: Nastavitve
+ direct_message_creator:
+ title: Novo zasebno sporočilo
+ prefix: "Do:"
+ create_channel:
+ type: "Tip"
+ types:
+ category: "Kategorija"
+ topic: "Tema"
+ composer:
+ italic_text: "poudarjeno"
+ bold_text: "krepko"
+ notification_levels:
+ never: "Nikoli"
+ settings:
+ follow: "Pridruži se"
+ followed: "Pridružen"
+ notifications: "Obvestila"
+ preview: "Predogled"
+ save: "Shrani"
+ saved: "Shranjeno"
+ unfollow: "Zapusti"
+ incoming_webhooks:
+ back: "Nazaj"
+ description: "Opis"
+ delete: "Izbriši"
+ emoji: "Emoji"
+ name: "Ime"
+ save: "Shrani"
+ edit: "Uredi"
+ system: "sistem"
+ url: "URL"
+ username: "Uporabniško ime"
+ selection:
+ cancel: "Prekliči"
+ copy: "Kopiraj"
+ new_topic:
+ title: "Prestavi v novo temo"
+ existing_topic:
+ title: "Prestavi v obstoječo temo"
+ new_message:
+ title: "Prestavi v novo ZS"
+ emoji_picker:
+ objects: "Stvari"
+ activities: "Dejavnosti"
+ flags: "Zastave"
+ symbols: "Simboli"
+ draft_channel_screen:
+ header: "Novo zasebno sporočilo"
+ cancel: "Prekliči"
+ notifications:
+ chat_quoted: "%{username} %{description}"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ fields:
+ message:
+ label: Opozorilo
+ review:
+ types:
+ reviewable_chat_message:
+ flagged_by: "Prijavljen od"
diff --git a/plugins/chat/config/locales/client.sq.yml b/plugins/chat/config/locales/client.sq.yml
new file mode 100644
index 0000000000..9bd366f3be
--- /dev/null
+++ b/plugins/chat/config/locales/client.sq.yml
@@ -0,0 +1,95 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+sq:
+ js:
+ chat:
+ cancel: "Anulo"
+ channel_settings:
+ edit: "Redakto"
+ add: "Shto"
+ close: "Mbyll"
+ delete: "Fshij"
+ muted: "të heshtur"
+ joined: "anëtarësuar"
+ email_frequency:
+ never: "Asnjëherë"
+ flag: "Sinjalizoni"
+ mention_warning:
+ dismiss: "hiqe"
+ reply: "Përgjigju"
+ edit: "Redakto"
+ rebake_message: "Rindërtoni HTML"
+ bookmark_message: "Shto tek të preferuarat"
+ save: "Ruaj"
+ sounds:
+ none: "Asnjë"
+ exit: "kthehu mbrapa"
+ channel_status:
+ open: "Fillo"
+ browse:
+ back: "Kthehu mbrapa"
+ filter_all: Të Gjithë
+ chat_message_separator:
+ today: Sot
+ yesterday: Dje
+ about_view:
+ title: Titulli
+ description: Përshkrimi
+ channel_info:
+ back_to_channel: "Kthehu mbrapa"
+ tabs:
+ about: Rreth
+ members: Anëtarë
+ settings: Rregullimet
+ direct_message_creator:
+ title: Mesazh i ri
+ prefix: "Për:"
+ create_channel:
+ type: "Lloji"
+ types:
+ category: "Kategori"
+ topic: "Topic"
+ composer:
+ italic_text: "tekst i theksuar"
+ bold_text: "tekst i trashë"
+ notification_levels:
+ never: "Asnjëherë"
+ settings:
+ followed: "Anëtarësuar"
+ notifications: "Njoftimet"
+ preview: "Parashikimi"
+ save: "Ruaj"
+ saved: "U ruajt"
+ incoming_webhooks:
+ back: "Kthehu mbrapa"
+ description: "Përshkrimi"
+ delete: "Fshije"
+ emoji: "Emoji"
+ name: "Emri juaj"
+ save: "Ruaj"
+ edit: "Redakto"
+ system: "sistem"
+ url: "URL"
+ username: "Emri i përdoruesit"
+ selection:
+ cancel: "Anulo"
+ copy: "Kopjo"
+ new_topic:
+ title: "Ktheje në një temë të re"
+ existing_topic:
+ title: "Transfero tek një Temë tjetër"
+ emoji_picker:
+ flags: "Sinjalizime"
+ draft_channel_screen:
+ header: "Mesazh i ri"
+ cancel: "Anulo"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ fields:
+ message:
+ label: Mesazh
diff --git a/plugins/chat/config/locales/client.sr.yml b/plugins/chat/config/locales/client.sr.yml
new file mode 100644
index 0000000000..6659cccd58
--- /dev/null
+++ b/plugins/chat/config/locales/client.sr.yml
@@ -0,0 +1,96 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+sr:
+ js:
+ chat:
+ dates:
+ time_tiny: "h:mm"
+ cancel: "Odustani"
+ channel_settings:
+ edit: "Izmeni"
+ add: "Dodaj"
+ close: "Zatvori"
+ delete: "Obriši"
+ muted: "utišano"
+ joined: "pridružen"
+ email_frequency:
+ never: "Nikad"
+ flag: "Označi Zastavom"
+ mention_warning:
+ dismiss: "одбаци"
+ reply: "Odgovori"
+ edit: "Izmeni"
+ rebake_message: "Popravi HTML"
+ bookmark_message: "Markiraj"
+ bookmark_message_edit: "Uredi marker"
+ save: "Sačuvaj"
+ sounds:
+ none: "Ništa"
+ exit: "nazad"
+ channel_status:
+ open: "Otvori"
+ browse:
+ back: "Nazad"
+ filter_all: sve
+ chat_message_separator:
+ today: Danas
+ yesterday: Juče
+ about_view:
+ title: Naslov
+ description: Opis
+ channel_info:
+ back_to_channel: "Nazad"
+ tabs:
+ about: O nama
+ members: Članovi
+ settings: Podešavanja
+ direct_message_creator:
+ title: Nova privatna poruka
+ create_channel:
+ type: "Tip"
+ types:
+ category: "Kategorija"
+ topic: "Tema"
+ composer:
+ italic_text: "italic tekst"
+ bold_text: "boldovan tekst"
+ notification_levels:
+ never: "Nikad"
+ settings:
+ followed: "Pridružio"
+ notifications: "Obaveštenja"
+ save: "Sačuvaj"
+ saved: "Sačuvano"
+ incoming_webhooks:
+ back: "Nazad"
+ description: "Opis"
+ delete: "Obriši"
+ emoji: "Emoji"
+ name: "Ime foruma"
+ save: "Sačuvaj"
+ edit: "Izmeni"
+ system: "sistem"
+ url: "URL"
+ username: "Korisničko Ime"
+ selection:
+ cancel: "Odustani"
+ copy: "Kopija"
+ new_topic:
+ title: "Prebaci u Novu Temu"
+ existing_topic:
+ title: "Prebaci u Postojeću Temu"
+ emoji_picker:
+ flags: "Zastave"
+ draft_channel_screen:
+ header: "Nova privatna poruka"
+ cancel: "Odustani"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ fields:
+ message:
+ label: Privatna poruka
diff --git a/plugins/chat/config/locales/client.sv.yml b/plugins/chat/config/locales/client.sv.yml
new file mode 100644
index 0000000000..f0f030f5bf
--- /dev/null
+++ b/plugins/chat/config/locales/client.sv.yml
@@ -0,0 +1,450 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+sv:
+ js:
+ admin:
+ logs:
+ staff_actions:
+ actions:
+ chat_channel_status_change: "Chattkanalens status har ändrats"
+ chat_channel_delete: "Chattkanal raderad"
+ api:
+ scopes:
+ descriptions:
+ chat:
+ create_message: "Skapa ett chattmeddelande i en angiven kanal."
+ about:
+ chat_messages_count: "Chattmeddelanden"
+ chat_channels_count: "Chattkanaler"
+ chat_users_count: "Chattanvändare"
+ chat:
+ dates:
+ time_tiny: "h:mm"
+ all_loaded: "Visar alla meddelanden"
+ already_enabled: "Chatt är redan aktiverat för detta ämne. Vänligen uppdatera."
+ disabled_for_topic: "Chatt är inaktiverat för detta ämne."
+ bot: "bot"
+ create: "Skapa"
+ cancel: "Avbryt"
+ cancel_reply: "Avbryt svar"
+ chat_channels: "Kanaler"
+ browse_all_channels: "Bläddra bland alla kanaler"
+ move_to_channel:
+ title: "Flytta meddelanden till kanal"
+ instructions:
+ one: "Du flyttar %{count} meddelande. Välj en destinationskanal. Ett platshållarmeddelande kommer att skapas i kanalen %{channelTitle} för att indikera att detta meddelande har flyttats."
+ other: "Du flyttar %{count} meddelanden. Välj en destinationskanal. Ett platshållarmeddelande kommer att skapas i kanalen %{channelTitle} för att indikera att dessa meddelanden har flyttats."
+ confirm_move: "Flytta meddelanden"
+ channel_settings:
+ title: "Kanalinställningar"
+ edit: "Redigera"
+ add: "Lägg till"
+ close_channel: "Stäng kanal"
+ open_channel: "Öppna kanal"
+ archive_channel: "Arkivera kanal"
+ delete_channel: "Radera kanal"
+ join_channel: "Gå med i kanal"
+ leave_channel: "Lämna kanal"
+ join: "Gå med"
+ leave: "Lämna"
+ channel_archive:
+ title: "Arkivera kanal"
+ instructions: "/hooks/:key -slutpunkten. Försändelsen består av en enda text-parameter, som är begränsad till 2000 tecken.
Vi stöder också begränsade Slack-formaterade text-parametrar, extraherar länkar och omnämnanden baserat på formatet på https://api.slack.com/reference/surfaces/formatting, men /hooks/:key/ slack-slutpunkten måste användas för detta."
+ selection:
+ cancel: "Avbryt"
+ quote_selection: "Citat i ämne"
+ copy: "Kopiera"
+ move_selection_to_channel: "Flytta till kanal"
+ error: "Det uppstod ett fel när chattmeddelanden skulle flyttas"
+ title: "Flytta chatt till ämne"
+ new_topic:
+ title: "Flytta till nytt ämne"
+ instructions:
+ one: "Du håller på att skapa ett nytt ämne och fylla det med chattmeddelandet du har valt."
+ other: "Du håller på att skapa ett nytt ämne och fylla det med de %{count} chattmeddelanden du har valt."
+ instructions_channel_archive: "Du håller på att skapa ett nytt ämne och arkivera kanalmeddelandena till det."
+ existing_topic:
+ title: "Flytta till befintligt ämne"
+ instructions:
+ one: "Välj det ämne du vill flytta chattmeddelandet till."
+ other: "Välj det ämne du vill flytta dessa %{count} chattmeddelanden till."
+ instructions_channel_archive: "Välj vilket ämne du vill arkivera kanalmeddelanden till."
+ new_message:
+ title: "Flytta till nytt meddelande"
+ instructions:
+ one: "Du håller på att skapa ett nytt meddelande och fylla det med chattmeddelandet du har valt."
+ other: "Du håller på att skapa ett nytt ämne och fylla det med de %{count} chattmeddelanden du har valt."
+ replying_indicator:
+ single_user: "%{username} skriver"
+ multiple_users: "%{commaSeparatedUsernames} och %{lastUsername} skriver"
+ many_users:
+ one: "%{commaSeparatedUsernames} och %{count} skriver"
+ other: "%{commaSeparatedUsernames} och %{count} andra skriver"
+ retention_reminders:
+ public: "Kanalhistoriken behålls i %{days} dagar."
+ dm: "Personlig chatthistorik behålls i %{days} dagar."
+ topic_button_title: "Chatt"
+ flags:
+ off_topic: "Det här meddelandet är inte relevant för den aktuella diskussionen enligt kanaltiteln och bör förmodligen flyttas någon annanstans."
+ inappropriate: "Detta meddelande innehåller saker som en förnuftig person skulle anse vara stötande, kränkande eller en överträdelse av vårt forums riktlinjer."
+ spam: "Det här meddelandet är en annons eller vandalism. Det är inte lämpligt eller relevant med avseende på den aktuella kanalen."
+ notify_user: "Jag vill prata med den här personen direkt och privat om meddelandet."
+ notify_moderators: "Detta meddelande kräver personalens uppmärksamhet av en annan anledning som inte anges ovan."
+ flagging:
+ action: "Flagga meddelande"
+ emoji_picker:
+ favorites: "Används ofta"
+ smileys_&_emotion: "Smileys och emojis"
+ objects: "Objekt"
+ people_&_body: "Människor och kropp"
+ travel_&_places: "Resor och platser"
+ animals_&_nature: "Djur och natur"
+ food_&_drink: "Mat och dryck"
+ activities: "Aktiviteter"
+ flags: "Flaggor"
+ symbols: "Symboler"
+ search_placeholder: "Sök efter emojinamn och alias..."
+ no_results: "Inga resultat"
+ draft_channel_screen:
+ header: "Nytt meddelande"
+ cancel: "Avbryt"
+ notifications:
+ chat_invitation: "bjöd in dig att gå med i en chattkanal"
+ chat_invitation_html: "%{username} bjöd in dig att gå med i en chattkanal"
+ chat_quoted: "%{username} %{description}"
+ popup:
+ chat_mention:
+ direct: 'nämnde dig i "%{channel}"'
+ direct_html: '%{username} nämnde dig i "%{channel}"'
+ other_plain: 'nämnde %{identifier} i "%{channel}"'
+ other_html: '%{username} nämnde %{identifier} i "%{channel}"'
+ direct_message_chat_mention:
+ direct: "nämnde dig i personlig chatt"
+ direct_html: "%{username} nämnde dig i en personlig chatt"
+ other_plain: "nämnde %{identifier} i personlig chatt"
+ other_html: "%{username} nämnde %{identifier} i personlig chatt"
+ chat_message: "Nytt chattmeddelande"
+ chat_quoted: "%{username} citerade ditt chattmeddelande"
+ titles:
+ chat_mention: "Chatt omnämnande"
+ chat_invitation: "Chattinbjudan"
+ chat_quoted: "Chatt citerad"
+ action_codes:
+ chat:
+ enabled: '%{who} aktiverade %{when}'
+ disabled: "%{who} stängde chatten %{when}"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ title: Skicka chattmeddelande
+ fields:
+ chat_channel_id:
+ label: Chattkanal-ID
+ message:
+ label: Meddelande
+ sender:
+ label: Avsändare
+ description: Standard är system
+ review:
+ transcript:
+ view: "Visa tidigare meddelandens avskrift"
+ types:
+ reviewable_chat_message:
+ title: "Flaggat chattmeddelande"
+ flagged_by: "Flaggat av"
+ keyboard_shortcuts_help:
+ chat:
+ title: "Chatt"
+ keyboard_shortcuts:
+ switch_channel_arrows: "%{shortcut} Byt kanal"
+ open_quick_channel_selector: "%{shortcut} Öppna snabbval av kanal"
+ open_insert_link_modal: "%{shortcut} Infoga hyperlänk (endast kompositör)"
+ composer_bold: "%{shortcut} Fet (endast kompositör)"
+ composer_italic: "%{shortcut} Kursiv (endast kompositör)"
+ composer_code: "%{shortcut} Kod (endast kompositör)"
+ drawer_open: "%{shortcut} Öppna chattmenyn"
+ drawer_close: "%{shortcut} Stäng chattmenyn"
+ topic_statuses:
+ chat:
+ help: "Chatt är aktiverat för detta ämne"
+ user:
+ allow_private_messages: "Tillåt andra användare att skicka mig personliga meddelanden och chattmeddelanden"
+ muted_users_instructions: "Neka alla meddelanden, personliga meddelanden och direkta chattmeddelanden från dessa användare."
+ allowed_pm_users_instructions: "Tillåt endast personliga meddelanden eller chattmeddelanden från dessa användare."
+ allow_private_messages_from_specific_users: "Tillåt endast specifika användare att skicka personliga meddelanden eller chattmeddelanden"
+ ignored_users_instructions: "Neka alla inlägg, meddelanden, notifikationer, personliga meddelanden eller direkta chattmeddelanden från dessa användare."
+ user_menu:
+ no_chat_notifications_title: "Du har inga chattaviseringar än"
+ no_chat_notifications_body: >
+ Du kommer att aviseras i den här panelen när någon skickar direktmeddelanden till dig eller @nämner dig i chatten. Aviseringar kommer också att skickas till din e-postadress när du inte har loggat in på ett tag.
Klicka på titeln överst på valfri chattkanal för att konfigurera vilka aviseringar du får i den kanalen. För mer, se dina aviseringsinställningar.
+ tabs:
+ chat_notifications: "Chattaviseringar"
+ chat_notifications_with_unread:
+ one: "Chattaviseringar - %{count} oläst avisering"
+ other: "Chattaviseringar - %{count} olästa aviseringar"
diff --git a/plugins/chat/config/locales/client.sw.yml b/plugins/chat/config/locales/client.sw.yml
new file mode 100644
index 0000000000..394c238c4d
--- /dev/null
+++ b/plugins/chat/config/locales/client.sw.yml
@@ -0,0 +1,104 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+sw:
+ js:
+ chat:
+ create: "Tengeneza"
+ cancel: "Ghairi"
+ channel_settings:
+ edit: "Hariri"
+ add: "ongeza"
+ join: "Jiunge"
+ leave: "Ondoka"
+ close: "Funga"
+ delete: "Futa"
+ muted: "kunyamazisha"
+ joined: "alijiunga"
+ email_frequency:
+ never: "Kamwe"
+ flag: "Bendera"
+ join: "Jiunge"
+ mention_warning:
+ dismiss: "ondosha..."
+ reply: "Jibu"
+ edit: "Hariri"
+ rebake_message: "Tengeneza upya HTML"
+ bookmark_message: "Alamisha"
+ save: "Hifadhi"
+ sounds:
+ none: "Hakuna"
+ exit: "iliyopita"
+ channel_status:
+ closed: "Imefungwa"
+ open: "Fungua"
+ browse:
+ back: "Iliyopita"
+ filter_all: Vyote
+ filter_closed: Imefungwa
+ chat_message_separator:
+ today: Leo
+ yesterday: Jana
+ about_view:
+ title: Kichwa cha Habari
+ description: Elezo
+ channel_info:
+ back_to_channel: "Iliyopita"
+ tabs:
+ about: Kuhusu
+ members: Wanachama
+ settings: Mipangilio
+ direct_message_creator:
+ title: Ujumbe Mpya
+ prefix: "Kwenda:"
+ create_channel:
+ type: "Aina"
+ types:
+ category: "Kikundi"
+ topic: "Mada"
+ composer:
+ italic_text: "Maneno yaliyo tiliwa mkazo"
+ bold_text: "Maneno yaliyokolezwa"
+ notification_levels:
+ never: "Kamwe"
+ settings:
+ follow: "Jiunge"
+ followed: "Alijiunga"
+ notifications: "Taarifa"
+ preview: "Kihakiki"
+ save: "Hifadhi"
+ saved: "Imehifadhiwa"
+ unfollow: "Ondoka"
+ incoming_webhooks:
+ back: "Iliyopita"
+ description: "Elezo"
+ delete: "Futa"
+ emoji: "Emoji"
+ name: "Jina"
+ save: "Hifadhi"
+ edit: "Hariri"
+ system: "mfumo"
+ url: "Anwani ya mtandao"
+ username: "Jina la mtumiaji"
+ selection:
+ cancel: "Ghairi"
+ copy: "Nakili"
+ new_topic:
+ title: "Hamisha kwenda Mada Mpya"
+ existing_topic:
+ title: "Hamisha kwenda kwenye Mada Iliyopo"
+ emoji_picker:
+ objects: "Vitu"
+ flags: "Bendera"
+ draft_channel_screen:
+ header: "Ujumbe Mpya"
+ cancel: "Ghairi"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ fields:
+ message:
+ label: Ujumbe
diff --git a/plugins/chat/config/locales/client.te.yml b/plugins/chat/config/locales/client.te.yml
new file mode 100644
index 0000000000..37bc30b0b3
--- /dev/null
+++ b/plugins/chat/config/locales/client.te.yml
@@ -0,0 +1,73 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+te:
+ js:
+ chat:
+ cancel: "రద్దుచేయి"
+ channel_settings:
+ edit: "సవరణ"
+ add: "కలుపు"
+ close: "మూసివేయి"
+ delete: "తొలగించు"
+ muted: "నిశ్శబ్దం"
+ joined: "చేరినారు"
+ flag: "కేతనం"
+ reply: "జవాబు"
+ edit: "సవరణ"
+ rebake_message: "హెచే టీ యం యల్ పునర్నిర్మించు"
+ bookmark_message: "పేజీక"
+ save: "భద్రపరుచు"
+ exit: "వెనుకకు"
+ browse:
+ back: "వెనుకకు"
+ filter_all: అన్ని
+ chat_message_separator:
+ today: ఈరోజు
+ yesterday: నిన్న
+ about_view:
+ title: శీర్షిక
+ description: వివరణ
+ channel_info:
+ back_to_channel: "వెనుకకు"
+ tabs:
+ about: గురించి
+ members: సభ్యులు
+ settings: అమరికలు
+ create_channel:
+ type: "రకం"
+ types:
+ category: "వర్గం"
+ topic: "విషయం"
+ composer:
+ italic_text: "వాలు పాఠ్యం"
+ bold_text: "బొద్దు పాఠ్యం"
+ settings:
+ followed: "చేరినారు"
+ notifications: "ప్రకటనలు"
+ save: "భద్రపరుచు"
+ incoming_webhooks:
+ back: "వెనుకకు"
+ description: "వివరణ"
+ delete: "తొలగించు"
+ emoji: "ఇమోజి"
+ name: "పేరు"
+ save: "భద్రపరుచు"
+ edit: "సవరణ"
+ system: "వ్వవస్థ"
+ url: "యూఆర్ యల్"
+ username: "సభ్యనామం"
+ selection:
+ cancel: "రద్దుచేయి"
+ copy: "నకలు"
+ new_topic:
+ title: "కొత్త విషయానికి జరుపు"
+ existing_topic:
+ title: "ఇప్పటికే ఉన్న విషయానికి జరుపు"
+ emoji_picker:
+ flags: "కేతనాలు"
+ draft_channel_screen:
+ cancel: "రద్దుచేయి"
diff --git a/plugins/chat/config/locales/client.th.yml b/plugins/chat/config/locales/client.th.yml
new file mode 100644
index 0000000000..dfc7d4e570
--- /dev/null
+++ b/plugins/chat/config/locales/client.th.yml
@@ -0,0 +1,115 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+th:
+ js:
+ chat:
+ create: "สร้าง"
+ cancel: "ยกเลิก"
+ channel_settings:
+ edit: "แก้ไข"
+ add: "เพิ่ม"
+ join: "เข้าร่วม"
+ leave: "ออก"
+ close: "ปิด"
+ delete: "ลบ"
+ edited: "ถูกแก้ไข"
+ muted: "ปิดการแจ้งเตือน"
+ joined: "สมัครสมาชิกเมื่อ"
+ email_frequency:
+ never: "ไม่เคย"
+ flag: "ธง"
+ join: "เข้าร่วม"
+ mention_warning:
+ dismiss: "ซ่อน"
+ reply: "ตอบ"
+ edit: "แก้ไข"
+ bookmark_message: "บุ๊คมาร์ค"
+ bookmark_message_edit: "แก้ไขบุ๊กมาร์ก"
+ save: "บันทึก"
+ sounds:
+ none: "ไม่มี"
+ exit: "กลับ"
+ channel_status:
+ closed: "ปิด"
+ open: "เปิด"
+ browse:
+ back: "กลับ"
+ filter_all: ทั้งหมด
+ filter_closed: ปิด
+ chat_message_separator:
+ today: วันนี้
+ yesterday: เมื่อวาน
+ about_view:
+ title: ชื่อเรื่อง
+ description: รายละเอียด
+ channel_info:
+ back_to_channel: "กลับ"
+ tabs:
+ about: เกี่ยวกับ
+ members: สมาชิก
+ settings: การตั้งค่า
+ direct_message_creator:
+ title: สร้างข้อความใหม่
+ prefix: "ถึง:"
+ create_channel:
+ type: "ชนิด"
+ types:
+ category: "หมวดหมู่"
+ topic: "หัวข้อ"
+ composer:
+ italic_text: "ตัวอักษรเอียง"
+ bold_text: "ตัวอักษรหนา"
+ notification_levels:
+ never: "ไม่เคย"
+ settings:
+ follow: "เข้าร่วม"
+ followed: "สมัครสมาชิกเมื่อ"
+ notifications: "การแจ้งเตือน"
+ preview: "แสดงตัวอย่าง"
+ save: "บันทึก"
+ saved: "บันทึกแล้ว"
+ unfollow: "ออก"
+ incoming_webhooks:
+ back: "กลับ"
+ description: "รายละเอียด"
+ delete: "ลบ"
+ emoji: "Emoji"
+ name: "ชื่อ"
+ save: "บันทึก"
+ edit: "แก้ไข"
+ system: "ระบบ"
+ url: "URL"
+ username: "ชื่อผู้ใช้"
+ selection:
+ cancel: "ยกเลิก"
+ copy: "คัดลอก"
+ new_topic:
+ title: "ย้ายไปกระทู้ใหม่"
+ existing_topic:
+ title: "ย้ายไปกระทู้ที่มีอยู่แล้ว"
+ new_message:
+ title: "ย้ายไปข้อความใหม่"
+ emoji_picker:
+ objects: "วัตถุ"
+ activities: "กิจกรรม"
+ flags: "ธง"
+ symbols: "สัญลักษณ์"
+ draft_channel_screen:
+ header: "สร้างข้อความใหม่"
+ cancel: "ยกเลิก"
+ notifications:
+ chat_quoted: "%{username}%{description}"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ fields:
+ message:
+ label: ข้อความ
+ review:
+ types:
+ reviewable_chat_message:
+ flagged_by: "ถูกปักธงโดย"
diff --git a/plugins/chat/config/locales/client.tr_TR.yml b/plugins/chat/config/locales/client.tr_TR.yml
new file mode 100644
index 0000000000..ce1dcfe818
--- /dev/null
+++ b/plugins/chat/config/locales/client.tr_TR.yml
@@ -0,0 +1,115 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+tr_TR:
+ js:
+ chat:
+ create: "Oluştur"
+ cancel: "İptal et"
+ channel_settings:
+ edit: "Düzenle"
+ add: "Ekle"
+ join: "Katıl"
+ leave: "Ayrıl"
+ close: "Bitir"
+ delete: "Sil"
+ edited: "düzenlendi"
+ muted: "sessiz"
+ joined: "katılma Tarihi"
+ email_frequency:
+ never: "Asla"
+ flag: "Bayrak Koy"
+ join: "Katıl"
+ mention_warning:
+ dismiss: "kapat"
+ reply: "Yanıtla"
+ edit: "Düzenle"
+ rebake_message: "HTML'i Yeniden Yapılandır"
+ bookmark_message: "Yer İmi"
+ bookmark_message_edit: "Yer İmini Düzenle"
+ save: "Kaydet"
+ sounds:
+ none: "Yok"
+ exit: "geri"
+ channel_status:
+ closed: "Kapanmış"
+ open: "Başlat"
+ browse:
+ filter_all: Hepsi
+ filter_closed: Kapanmış
+ chat_message_separator:
+ today: Bugün
+ yesterday: Dün
+ about_view:
+ title: Başlık
+ description: Açıklama
+ channel_info:
+ back_to_channel: "Geri"
+ tabs:
+ about: Hakkında
+ members: Üyeler
+ settings: Ayarlar
+ direct_message_creator:
+ title: Yeni İleti
+ prefix: "Kime:"
+ create_channel:
+ type: "Tür"
+ types:
+ category: "Kategori"
+ topic: "Konu"
+ composer:
+ italic_text: "vurgulanan yazı"
+ bold_text: "güçlü metin"
+ notification_levels:
+ never: "Asla"
+ settings:
+ follow: "Katıl"
+ followed: "Katılma Tarihi"
+ notifications: "Bildirimler"
+ preview: "Önizleme"
+ save: "Kaydet"
+ saved: "Kaydedildi"
+ unfollow: "Ayrıl"
+ incoming_webhooks:
+ back: "Geri"
+ description: "Açıklama"
+ delete: "Sil"
+ emoji: "Emoji"
+ name: "Ad"
+ save: "Kaydet"
+ edit: "Düzenle"
+ system: "sistem"
+ url: "URL"
+ username: "Kullanıcı Adı"
+ selection:
+ cancel: "İptal et"
+ copy: "Kopyala"
+ new_topic:
+ title: "Yeni Konuya Geç"
+ existing_topic:
+ title: "Var Olan Bir Konuya Taşı"
+ new_message:
+ title: "Yeni Mesajlara Taşı"
+ emoji_picker:
+ objects: "Nesneler"
+ activities: "Faaliyetler"
+ flags: "Bildirilenler"
+ symbols: "Semboller"
+ draft_channel_screen:
+ header: "Yeni İleti"
+ cancel: "İptal et"
+ notifications:
+ chat_quoted: "%{username} %{description}"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ fields:
+ message:
+ label: İleti
+ review:
+ types:
+ reviewable_chat_message:
+ flagged_by: "Bildiren"
diff --git a/plugins/chat/config/locales/client.uk.yml b/plugins/chat/config/locales/client.uk.yml
new file mode 100644
index 0000000000..c0b3963211
--- /dev/null
+++ b/plugins/chat/config/locales/client.uk.yml
@@ -0,0 +1,116 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+uk:
+ js:
+ chat:
+ create: "Створити"
+ cancel: "Скасувати"
+ channel_settings:
+ edit: "Редагувати"
+ add: "Додати"
+ join: "Приєднатися"
+ leave: "Покинути"
+ close: "Закрити"
+ delete: "Видалити"
+ edited: "відредагований"
+ muted: "ігноровані"
+ joined: "приєднався(лась)"
+ email_frequency:
+ never: "Ніколи"
+ flag: "Поскаржитися"
+ join: "Приєднатися"
+ mention_warning:
+ dismiss: "відкласти"
+ reply: "Відповідь"
+ edit: "Редагувати"
+ rebake_message: "Перебудувати HTML"
+ bookmark_message: "Додати закладку"
+ bookmark_message_edit: "Редагувати закладку"
+ save: "Зберегти"
+ sounds:
+ none: "Немає"
+ exit: "назад"
+ channel_status:
+ closed: "Закриті"
+ open: "Відкрити"
+ browse:
+ back: "Назад"
+ filter_all: Все
+ filter_closed: Закриті
+ chat_message_separator:
+ today: Сьогодні
+ yesterday: Вчора
+ about_view:
+ title: Назва
+ description: Опис
+ channel_info:
+ back_to_channel: "Назад"
+ tabs:
+ about: Про
+ members: Учасники
+ settings: Налаштування
+ direct_message_creator:
+ title: Нове повідомлення
+ prefix: "До:"
+ create_channel:
+ type: "Тип"
+ types:
+ category: "Розділ"
+ topic: "Тема"
+ composer:
+ italic_text: "виділення тексту курсивом"
+ bold_text: "Сильне виділення тексту"
+ notification_levels:
+ never: "Ніколи"
+ settings:
+ follow: "Приєднатися"
+ followed: "Приєднався(лась)"
+ notifications: "Сповіщення"
+ preview: "Попередній перегляд"
+ save: "Зберегти"
+ saved: "Збережено"
+ unfollow: "Покинути"
+ incoming_webhooks:
+ back: "Назад"
+ description: "Опис"
+ delete: "Видалити"
+ emoji: "Смайли"
+ name: "Назва"
+ save: "Зберегти"
+ edit: "Редагувати"
+ system: "системні"
+ url: "Посилання"
+ username: "Ім’я користувача"
+ selection:
+ cancel: "Скасувати"
+ copy: "Копіювати"
+ new_topic:
+ title: "Перенесення до нової теми"
+ existing_topic:
+ title: "Перенесення до наявної теми"
+ new_message:
+ title: "Перейти до нового повідомленням"
+ emoji_picker:
+ objects: "Objects"
+ activities: "Діяльність"
+ flags: "Скарги"
+ symbols: "Атрибутика"
+ draft_channel_screen:
+ header: "Нове повідомлення"
+ cancel: "Скасувати"
+ notifications:
+ chat_quoted: "%{username} %{description}"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ fields:
+ message:
+ label: Повідомлення
+ review:
+ types:
+ reviewable_chat_message:
+ flagged_by: "Позначено"
diff --git a/plugins/chat/config/locales/client.ur.yml b/plugins/chat/config/locales/client.ur.yml
new file mode 100644
index 0000000000..55dc84702b
--- /dev/null
+++ b/plugins/chat/config/locales/client.ur.yml
@@ -0,0 +1,116 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+ur:
+ js:
+ chat:
+ create: "بنائیں"
+ cancel: "منسوخ"
+ channel_settings:
+ edit: "ترمیم کریں"
+ add: "اضافہ کریں"
+ join: "شمولیت اختیار کریں"
+ leave: "چھوڑ دیں"
+ close: "بند کریں"
+ delete: "مٹائیں"
+ edited: "ترمیم کردہ"
+ muted: "خاموش کِیا ہوا"
+ joined: "شمولیت اختیار کی"
+ email_frequency:
+ never: "کبھی نہیں"
+ flag: "فلَیگ"
+ join: "شمولیت اختیار کریں"
+ mention_warning:
+ dismiss: "بر خاست کریں"
+ reply: "جواب"
+ edit: "ترمیم کریں"
+ rebake_message: "HTML دوبارہ بِلڈ کریں"
+ bookmark_message: "بُک مارک"
+ bookmark_message_edit: "بک مارک میں ترمیم کریں"
+ save: "محفوظ کریں"
+ sounds:
+ none: "کوئی نہیں"
+ exit: "واپس"
+ channel_status:
+ closed: "بند"
+ open: "کھولیں"
+ browse:
+ back: "واپس"
+ filter_all: تمام
+ filter_closed: بند
+ chat_message_separator:
+ today: آج
+ yesterday: کَل
+ about_view:
+ title: عنوان
+ description: تفصیل
+ channel_info:
+ back_to_channel: "واپس"
+ tabs:
+ about: بارے میں
+ members: ممبران
+ settings: ترتیبات
+ direct_message_creator:
+ title: نیا پیغام
+ prefix: "کے لئے:"
+ create_channel:
+ type: "قِسم"
+ types:
+ category: "زمرہ"
+ topic: "ٹاپک"
+ composer:
+ italic_text: "زور دیا گیا ٹَیکسٹ"
+ bold_text: "گہرا ٹَیکسٹ"
+ notification_levels:
+ never: "کبھی نہیں"
+ settings:
+ follow: "شمولیت اختیار کریں"
+ followed: "شمولیت اختیار کی"
+ notifications: "اطلاعات"
+ preview: "پیشگی دیکھیں"
+ save: "محفوظ کریں"
+ saved: "محفوظ کر لیا گیا"
+ unfollow: "چھوڑ دیں"
+ incoming_webhooks:
+ back: "واپس"
+ description: "تفصیل"
+ delete: "مٹائیں"
+ emoji: "اِیمَوجی"
+ name: "نام"
+ save: "محفوظ کریں"
+ edit: "ترمیم کریں"
+ system: "سِسٹَم"
+ url: "URL"
+ username: "صارف نام"
+ selection:
+ cancel: "منسوخ"
+ copy: "کاپی"
+ new_topic:
+ title: "نئے ٹاپک پر منتقل کریں"
+ existing_topic:
+ title: "موجودہ ٹاپک میں منتقل کریں"
+ new_message:
+ title: "نئے پیغام پر منتقل کریں"
+ emoji_picker:
+ objects: "اشیاء"
+ activities: "سرگرمیاں"
+ flags: "فلَیگز"
+ symbols: "علامات"
+ draft_channel_screen:
+ header: "نیا پیغام"
+ cancel: "منسوخ"
+ notifications:
+ chat_quoted: "%{username} %{description}"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ fields:
+ message:
+ label: پیغام
+ review:
+ types:
+ reviewable_chat_message:
+ flagged_by: "کی طرف سے فلَیگ کردہ"
diff --git a/plugins/chat/config/locales/client.vi.yml b/plugins/chat/config/locales/client.vi.yml
new file mode 100644
index 0000000000..d698006d06
--- /dev/null
+++ b/plugins/chat/config/locales/client.vi.yml
@@ -0,0 +1,116 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+vi:
+ js:
+ chat:
+ create: "Tạo"
+ cancel: "Huỷ"
+ channel_settings:
+ edit: "Sửa"
+ add: "Thêm"
+ join: "Tham gia"
+ leave: "Rời nhóm"
+ close: "Đóng"
+ delete: "Xóa"
+ edited: "đã chỉnh sửa"
+ muted: "im lặng"
+ joined: "đã tham gia"
+ email_frequency:
+ never: "Không bao giờ"
+ flag: "Gắn cờ"
+ join: "Tham gia"
+ mention_warning:
+ dismiss: "bỏ qua"
+ reply: "Trả lời"
+ edit: "Sửa"
+ rebake_message: "Tạo lại HTML"
+ bookmark_message: "Đánh dấu chỉ mục"
+ bookmark_message_edit: "Chỉnh sửa Dấu trang"
+ save: "Lưu lại"
+ sounds:
+ none: "Không có gì"
+ exit: "quay lại"
+ channel_status:
+ closed: "Đã "
+ open: "Mở"
+ browse:
+ back: "Quay lại"
+ filter_all: All
+ filter_closed: Đã
+ chat_message_separator:
+ today: Hôm nay
+ yesterday: Hôm qua
+ about_view:
+ title: Tiêu đề
+ description: Mô tả
+ channel_info:
+ back_to_channel: "Quay lại"
+ tabs:
+ about: Giới thiệu
+ members: Các thành viên
+ settings: Cài đặt
+ direct_message_creator:
+ title: Tin nhắn mới
+ prefix: "Tới:"
+ create_channel:
+ type: "Loại"
+ types:
+ category: "Chuyên mục"
+ topic: "Chủ đề"
+ composer:
+ italic_text: "văn bản nhấn mạnh"
+ bold_text: "chữ in đậm"
+ notification_levels:
+ never: "Không bao giờ"
+ settings:
+ follow: "Tham gia"
+ followed: "Đã tham gia"
+ notifications: "Thông báo"
+ preview: "Xem trước"
+ save: "Lưu lại"
+ saved: "Lưu trữ"
+ unfollow: "Rời nhóm"
+ incoming_webhooks:
+ back: "Quay lại"
+ description: "Mô tả"
+ delete: "Xoá"
+ emoji: "Emoji"
+ name: "T"
+ save: "Lưu lại"
+ edit: "Sửa"
+ system: "hệ thống"
+ url: "URL"
+ username: "Tên tài khoản"
+ selection:
+ cancel: "Huỷ"
+ copy: "Sao chép"
+ new_topic:
+ title: "Di chuyển tới Chủ đề mới"
+ existing_topic:
+ title: "Di chuyển tới chủ đề đang tồn tại"
+ new_message:
+ title: "Chuyển đến tin nhắn mới"
+ emoji_picker:
+ objects: "Vật thể"
+ activities: "Hoạt động"
+ flags: "Dấu cờ - Flags"
+ symbols: "Ký hiệu"
+ draft_channel_screen:
+ header: "Tin nhắn mới"
+ cancel: "Huỷ"
+ notifications:
+ chat_quoted: "%{username} %{description}"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ fields:
+ message:
+ label: Tin nhắn
+ review:
+ types:
+ reviewable_chat_message:
+ flagged_by: "Gắn cờ bởi"
diff --git a/plugins/chat/config/locales/client.zh_CN.yml b/plugins/chat/config/locales/client.zh_CN.yml
new file mode 100644
index 0000000000..9974d2731d
--- /dev/null
+++ b/plugins/chat/config/locales/client.zh_CN.yml
@@ -0,0 +1,436 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+zh_CN:
+ js:
+ admin:
+ logs:
+ staff_actions:
+ actions:
+ chat_channel_status_change: "聊天频道状态已更改"
+ chat_channel_delete: "聊天频道已删除"
+ api:
+ scopes:
+ descriptions:
+ chat:
+ create_message: "在指定频道创建聊天消息。"
+ about:
+ chat_messages_count: "聊天消息"
+ chat_channels_count: "聊天频道"
+ chat_users_count: "聊天用户"
+ chat:
+ dates:
+ time_tiny: "h:mm"
+ all_loaded: "显示所有消息"
+ already_enabled: "此话题已启用聊天。请刷新。"
+ disabled_for_topic: "此话题已禁用聊天。"
+ bot: "机器人"
+ create: "创建"
+ cancel: "取消"
+ cancel_reply: "取消回复"
+ chat_channels: "频道"
+ browse_all_channels: "浏览所有频道"
+ move_to_channel:
+ title: "将消息移至频道"
+ instructions:
+ other: "您正在移动 %{count} 条消息。选择一个目标频道。 将在%{channelTitle}频道中创建一条占位符消息,以表明这些消息已被移动。"
+ confirm_move: "移动消息"
+ channel_settings:
+ title: "频道设置"
+ edit: "编辑"
+ add: "添加"
+ close_channel: "关闭频道"
+ open_channel: "打开频道"
+ archive_channel: "归档频道"
+ delete_channel: "删除频道"
+ join_channel: "加入频道"
+ leave_channel: "离开频道"
+ join: "加入"
+ leave: "离开"
+ channel_archive:
+ title: "归档频道"
+ instructions: "/hooks/:key 端点将消息发布到指定的聊天频道。有效负荷由单个 text 参数组成,限制为 2000 个字符。
我们还支持有限 Slack 格式的 text 参数以及基于 https://api.slack.com/reference/surfaces/formatting 中的格式提取链接和提及,但是必须为此使用 /hooks/:key/slack 端点。"
+ selection:
+ cancel: "取消"
+ quote_selection: "话题中的引用"
+ copy: "复制"
+ move_selection_to_channel: "移至频道"
+ error: "移动聊天消息时出错"
+ title: "将聊天移动到话题"
+ new_topic:
+ title: "移动到新话题"
+ instructions:
+ other: "您将创建一个新话题并使用您选择的 %{count} 条聊天消息进行填充。"
+ instructions_channel_archive: "您将要创建一个新话题并将频道消息归档到该话题。"
+ existing_topic:
+ title: "移动到现有话题"
+ instructions:
+ other: "请选择您要将这 %{count} 条聊天消息移动到的话题。"
+ instructions_channel_archive: "请选择您要将频道消息归档到的话题。"
+ new_message:
+ title: "移动到新消息"
+ instructions:
+ other: "您将创建一条新消息并使用您选择的 %{count} 条聊天消息进行填充。"
+ replying_indicator:
+ single_user: "%{username} 正在输入"
+ multiple_users: "%{commaSeparatedUsernames} 和 %{lastUsername} 正在输入"
+ many_users:
+ other: "%{commaSeparatedUsernames} 和其他 %{count} 人正在输入"
+ retention_reminders:
+ public: "频道历史记录保留 %{days} 天。"
+ dm: "个人聊天记录保留 %{days} 天。"
+ topic_button_title: "聊天"
+ flags:
+ off_topic: "此消息与频道标题定义的当前讨论无关,可能应当移至其他地方。"
+ inappropriate: "此消息包含理性的人会认为具有攻击性、辱骂性或违反我们的社区准则的内容。"
+ spam: "此消息是广告或破坏行为。它对当前频道没有用,也不相关。"
+ notify_user: "我想亲自直接与此人谈谈他们的消息。"
+ notify_moderators: "由于上面未列出的另一个原因,此消息需要管理人员加以注意。"
+ flagging:
+ action: "举报消息"
+ emoji_picker:
+ favorites: "常用"
+ smileys_&_emotion: "笑脸和情感"
+ objects: "物体"
+ people_&_body: "人和身体"
+ travel_&_places: "旅行和地点"
+ animals_&_nature: "动物和自然"
+ food_&_drink: "食品和饮料"
+ activities: "活动"
+ flags: "旗帜"
+ symbols: "符号"
+ search_placeholder: "按表情符号名称和别名搜索…"
+ no_results: "没有结果"
+ draft_channel_screen:
+ header: "新消息"
+ cancel: "取消"
+ notifications:
+ chat_invitation: "邀请您加入聊天频道"
+ chat_invitation_html: "%{username}邀请您加入一个聊天频道"
+ chat_quoted: "%{username} %{description}"
+ popup:
+ chat_mention:
+ direct: '在“%{channel}”中提及您'
+ direct_html: '%{username} 在“%{channel}”中提及您'
+ other_plain: '在“%{channel}”中提及“%{identifier}”'
+ other_html: '%{username} 在“%{channel}”中提及“%{identifier}”'
+ direct_message_chat_mention:
+ direct: "在个人聊天中提及您"
+ direct_html: "%{username} 在个人聊天中提及您"
+ other_plain: "在个人聊天中提及“%{identifier}”"
+ other_html: "%{username} 在个人聊天中提及“%{identifier}”"
+ chat_message: "新的聊天消息"
+ chat_quoted: "%{username} 引用了您的聊天消息"
+ titles:
+ chat_mention: "聊天提及"
+ chat_invitation: "聊天邀请"
+ chat_quoted: "已引用聊天"
+ action_codes:
+ chat:
+ enabled: '%{who} 于 %{when} 启用了'
+ disabled: "%{who} 于 %{when} 关闭了聊天"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ title: 发送聊天消息
+ fields:
+ chat_channel_id:
+ label: 聊天频道 ID
+ message:
+ label: 私信
+ sender:
+ label: 发送人
+ description: 默认为“系统”
+ review:
+ transcript:
+ view: "查看以前的消息副本"
+ types:
+ reviewable_chat_message:
+ title: "举报的聊天消息"
+ flagged_by: "举报者"
+ keyboard_shortcuts_help:
+ chat:
+ title: "聊天"
+ keyboard_shortcuts:
+ switch_channel_arrows: "%{shortcut} 切换频道"
+ open_quick_channel_selector: "%{shortcut} 打开快速频道选择器"
+ open_insert_link_modal: "%{shortcut} 插入超链接(仅输入框)"
+ composer_bold: "%{shortcut} 粗体(仅输入框)"
+ composer_italic: "%{shortcut} 斜体(仅输入框)"
+ composer_code: "%{shortcut} 代码(仅输入框)"
+ drawer_open: "%{shortcut} 打开聊天抽屉"
+ drawer_close: "%{shortcut} 关闭聊天抽屉"
+ topic_statuses:
+ chat:
+ help: "已为此话题启用聊天"
+ user:
+ allow_private_messages: "允许其他用户向我发送个人消息和聊天直接消息"
+ muted_users_instructions: "禁止来自这些用户的所有通知、个人消息和聊天消息。"
+ allowed_pm_users_instructions: "仅允许来自这些用户的个人消息或聊天直接消息。"
+ allow_private_messages_from_specific_users: "只允许特定用户向我发送个人消息或聊天直接消息"
+ ignored_users_instructions: "禁止来自这些用户的所有帖子、消息、通知、个人消息和聊天直接消息。"
+ user_menu:
+ no_chat_notifications_title: "您还没有任何聊天通知"
+ no_chat_notifications_body: >
+ 当有人在聊天中直接向您发送消息或提及 (@) 您时,您将在此面板中收到通知。当您有一段时间没有登录时,通知也会发送到您的电子邮件。
点击任何聊天频道顶部的标题以配置您在该频道中接收的通知。有关详情,请参阅您的通知偏好设置。
+ tabs:
+ chat_notifications: "聊天通知"
+ chat_notifications_with_unread:
+ other: "聊天通知 - %{count} 个未读通知"
diff --git a/plugins/chat/config/locales/client.zh_TW.yml b/plugins/chat/config/locales/client.zh_TW.yml
new file mode 100644
index 0000000000..e37dc401fe
--- /dev/null
+++ b/plugins/chat/config/locales/client.zh_TW.yml
@@ -0,0 +1,115 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+zh_TW:
+ js:
+ chat:
+ create: "創建"
+ cancel: "取消"
+ channel_settings:
+ edit: "編輯"
+ add: "加入"
+ join: "加入"
+ leave: "離開"
+ close: "關閉"
+ delete: "刪除"
+ muted: "靜音"
+ joined: "建立日期"
+ email_frequency:
+ never: "永不"
+ flag: "檢舉"
+ join: "加入"
+ mention_warning:
+ dismiss: "忽略"
+ reply: "回覆"
+ edit: "編輯"
+ rebake_message: "重建 HTML"
+ bookmark_message: "書籤"
+ bookmark_message_edit: "編輯書籤"
+ save: "保存"
+ sounds:
+ none: "無"
+ exit: "上一步"
+ channel_status:
+ closed: "不公開"
+ open: "開啟"
+ browse:
+ back: "上一步"
+ filter_all: 全部
+ filter_closed: 不公開
+ chat_message_separator:
+ today: 今天
+ yesterday: 昨天
+ about_view:
+ title: 標題
+ description: 簡述
+ channel_info:
+ back_to_channel: "上一步"
+ tabs:
+ about: 關於
+ members: 成員
+ settings: 設定
+ direct_message_creator:
+ title: 新訊息
+ prefix: "發至:"
+ create_channel:
+ type: "類型"
+ types:
+ category: "分類"
+ topic: "話題"
+ composer:
+ italic_text: "斜體字"
+ bold_text: "粗體字"
+ notification_levels:
+ never: "永不"
+ settings:
+ follow: "加入"
+ followed: "建立日期"
+ notifications: "通知"
+ preview: "預覽"
+ save: "保存"
+ saved: "已儲存"
+ unfollow: "離開"
+ incoming_webhooks:
+ back: "上一步"
+ description: "簡述"
+ delete: "刪除"
+ emoji: "Emoji"
+ name: "姓名"
+ save: "保存"
+ edit: "編輯"
+ system: "系統"
+ url: "網址"
+ username: "使用者名稱"
+ selection:
+ cancel: "取消"
+ copy: "複製"
+ new_topic:
+ title: "移至新話題"
+ existing_topic:
+ title: "移至已存在的話題"
+ new_message:
+ title: "移動到新訊息"
+ emoji_picker:
+ objects: "物品"
+ activities: "活動"
+ flags: "投訴"
+ symbols: "象徵
"
+ draft_channel_screen:
+ header: "新訊息"
+ cancel: "取消"
+ notifications:
+ chat_quoted: "%{username} %{description}"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ fields:
+ message:
+ label: 訊息
+ review:
+ types:
+ reviewable_chat_message:
+ flagged_by: "標記由"
diff --git a/plugins/chat/config/locales/server.ar.yml b/plugins/chat/config/locales/server.ar.yml
new file mode 100644
index 0000000000..ada4db21dc
--- /dev/null
+++ b/plugins/chat/config/locales/server.ar.yml
@@ -0,0 +1,216 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+ar:
+ site_settings:
+ chat_allowed_groups: "يمكن للمستخدمين في هذه المجموعات الدردشة. لاحظ أن أعضاء فريق العمل يمكنهم دائمًا الوصول إلى الدردشة."
+ chat_channel_retention_days: "سيتم الاحتفاظ برسائل الدردشة في القنوات العادية لهذا العدد من الأيام. اضبط القيمة على '0' للاحتفاظ بالرسائل إلى الأبد."
+ chat_dm_retention_days: "سيتم الاحتفاظ برسائل الدردشة في قنوات الدردشة الشخصية لهذا العدد من الأيام. اضبط القيمة على '0' للاحتفاظ بالرسائل إلى الأبد."
+ chat_auto_silence_duration: "عدد الدقائق التي سيتم فيها كتم المستخدمين عندما يتجاوزون حد معدل إنشاء رسائل الدردشة. اضبط القيمة على '0' لإيقاف الكتم التلقائي."
+ chat_allowed_messages_for_trust_level_0: "عدد الرسائل المسموح للمستخدمين من مستوى الثقة 0 بإرسالها خلال 30 ثانية. اضبط القيمة على '0' لإيقاف الحد."
+ chat_allowed_messages_for_other_trust_levels: "عدد الرسائل المسموح للمستخدمين من مستوى الثقة 1-4 بإرسالها خلال 30 ثانية. اضبط القيمة على '0' لإيقاف الحد."
+ chat_silence_user_sensitivity: "احتمالية أن يتم كتم المستخدم الذي تم الإبلاغ عنه في الدردشة تلقائيًا."
+ chat_auto_silence_from_flags_duration: "عدد الدقائق التي سيتم كتم المستخدمين تلقائيًا خلالها بسبب رسائل الدردشة التي تم الإبلاغ عنها."
+ chat_default_channel_id: "قناة الدردشة التي سيتم فتحها بشكلٍ افتراضي عندما لا يكون لدى المستخدم رسائل أو إشارات غير مقروءة في قنوات أخرى."
+ chat_duplicate_message_sensitivity: "احتمالية حظر الرسائل المكررة خلال فترة قصيرة من المُرسل نفسه. رقم عشري بين 0 و1.0، مع كون 1.0 هو أعلى إعداد (يحظر الرسائل بشكلٍ أكثر تكرارًا في فترة زمنية أقصر). اضبط القيمة على `0` للسماح بالرسائل المكررة."
+ chat_minimum_message_length: "الحد الأدنى لعدد الأحرف لرسالة دردشة."
+ chat_allow_uploads: "السماح بالتحميلات في قنوات الدردشة العامة وقنوات الرسائل المباشرة."
+ chat_archive_destination_topic_status: "الحالة التي يجب أن يكون عليها الموضوع المستهدف بعد اكتمال أرشيف القناة. ينطبق ذلك فقط عندما يكون الموضوع المستهدف موضوعًا جديدًا وليس موضوعًا موجودًا."
+ default_emoji_reactions: "تفاعلات الرموز التعبيرية الافتراضية لرسائل الدردشة. أضِف ما يصل إلى 5 رموز تعبيرية للتفاعل السريع."
+ direct_message_enabled_groups: "السماح للمستخدمين في تلك المجموعات بإنشاء دردشات شخصية بين مستخدم وآخر. ملاحظة: يمكن للموظفين دائمًا إنشاء دردشات شخصية، وسيتمكن المستخدمون من الرد على الدردشات الشخصية التي بدأها مستخدمون لديهم إذن بإنشائها."
+ chat_message_flag_allowed_groups: "السماح للمستخدمين في تلك المجموعات بالإبلاغ عن رسائل الدردشة."
+ errors:
+ chat_default_channel: "يجب أن تكون قناة الدردشة الافتراضية قناةً عامة."
+ direct_message_enabled_groups_invalid: "يجب عليك تحديد مجموعة واحدة على الأقل لهذا الإعداد. إذا كنت لا تريد أن يقوم أي شخص باستثناء فريق العمل بإرسال رسائل مباشرة، فاختر مجموعة فريق العمل."
+ chat_upload_not_allowed_secure_uploads: "غير مسموح بتحميلات الدردشة عندما يكون إعداد الموقع المسؤول عن التحميلات الآمنة مفعَّلًا."
+ system_messages:
+ chat_channel_archive_complete:
+ title: "اكتمل أرشيف قناة الدردشة"
+ subject_template: "اكتمل أرشيف قناة الدردشة بنجاح"
+ text_body_template: |
+ اكتملت أرشفة قناة الدردشة **\#%{channel_name}** بنجاح. وتم نسخ الرسائل إلى الموضوع [%{topic_title}](%{topic_url}).
+ chat_channel_archive_failed:
+ title: "فشلت أرشفة قناة الدردشة"
+ subject_template: "فشلت أرشفة قناة الدردشة"
+ text_body_template: |
+ فشلت أرشفة قناة الدردشة **\#%{channel_name}**. تم وضع الرسائل %{messages_archived} في الأرشيف. تم نسخ الرسائل المؤرشفة جزئيًا في الموضوع [%{topic_title}](%{topic_url}). انتقل إلى القناة في %{channel_url} لإعادة المحاولة.
+ chat:
+ deleted_chat_username: تم الحذف
+ errors:
+ channel_exists_for_category: "توجد قناة بالفعل في هذه الفئة وبهذا الاسم"
+ channel_new_message_disallowed: "القناة %{status}، لا يمكن إرسال رسائل جديدة"
+ channel_modify_message_disallowed: "القناة %{status}، لا يمكن تعديل أو حذف أي رسائل"
+ user_cannot_send_message: "لا يمكنك إرسال رسائل في الوقت الحالي."
+ rate_limit_exceeded: "تم تجاوز حد رسائل الدردشة التي يمكن إرسالها خلال 30 ثانية"
+ auto_silence_from_flags: "تم الإبلاغ عن الرسالة عددًا كافيًا من المرات لكتم المستخدم."
+ channel_cannot_be_archived: "لا يمكن أرشفة القناة في الوقت الحالي، يجب أن تكون إما مغلقة أو مفتوحة للأرشفة."
+ duplicate_message: "لقد نشرت رسالة مماثلة مؤخرًا."
+ delete_channel_failed: "فشل حذف القناة، يُرجى إعادة المحاولة."
+ minimum_length_not_met: "الرسالة قصيرة جدًا، ويجب ألا تقل عن %{minimum} من الأحرف."
+ max_reactions_limit_reached: "غير مسموح بتفاعلات جديدة على هذه الرسالة."
+ message_move_invalid_channel: "يجب أن تكون القناة المصدر والمستهدفة قناتين عامتين."
+ message_move_no_messages_found: "لم يتم العثور على رسائل بمعرِّفات الرسائل المقدَّمة."
+ cant_update_direct_message_channel: "لا يمكن تحديث خصائص قناة الرسائل المباشرة مثل الاسم والوصف."
+ not_accepting_dms: "عذرًا، لا يقبل %{username} الرسائل في الوقت الحالي."
+ actor_ignoring_target_user: "أنت تتجاهل %{username}؛ لذا لا يمكنك إرسال رسائل إليه."
+ actor_muting_target_user: "أنت تكتم %{username}؛ لذا لا يمكنك إرسال رسائل إليه."
+ actor_disallowed_dms: "لقد اخترت منع المستخدمين من إرسال رسائل خاصة ومباشرة إليك؛ لذا لا يمكنك إنشاء رسائل مباشرة جديدة."
+ actor_preventing_target_user_from_dm: "لقد اخترت منع %{username} من إرسال رسائل خاصة ومباشرة إليك؛ لذا لا يمكنك إنشاء رسائل مباشرة جديدة إليه."
+ user_cannot_send_direct_messages: "عذرًا، لا يمكنك إرسال الرسائل المباشرة."
+ reviewables:
+ message_already_handled: "شكرًا، لكننا راجعنا هذه الرسالة بالفعل وقرَّرنا أنك لست بحاجة إلى الإبلاغ عنها مرة أخرى."
+ actions:
+ agree:
+ title: "موافقة..."
+ agree_and_keep_message:
+ title: "الاحتفاظ بالرسالة"
+ description: "يمكنك الموافقة على البلاغ والاحتفاظ بالرسالة دون تغيير."
+ agree_and_keep_deleted:
+ title: "ترك الرسالة محذوفة"
+ description: "ويمكنك الموافقة على البلاغ وترك الرسالة محذوفة."
+ agree_and_suspend:
+ title: "تعليق المستخدم"
+ description: "يمكنك الموافقة على البلاغ وتعليق المستخدم."
+ agree_and_silence:
+ title: "كتم المستخدم"
+ description: "يمكنك الموافقة على البلاغ وكتم المستخدم."
+ agree_and_restore:
+ title: "استعادة الرسالة"
+ description: "يمكنك استعادة الرسالة حتى يتمكن المستخدمون من رؤيتها."
+ agree_and_delete:
+ title: "حذف الرسالة"
+ description: "يمكنك حذف الرسالة حتى لا يتمكن المستخدمون من رؤيتها."
+ delete_and_agree:
+ title: "حذف الرسالة"
+ disagree_and_restore:
+ title: "عدم الموافقة واستعادة الرسالة"
+ description: "يمكنك استعادة الرسالة حتى يتمكن جميع المستخدمين من رؤيتها."
+ disagree:
+ title: "عدم الموافقة"
+ ignore:
+ title: "تجاهل"
+ direct_messages:
+ transcript_title: "نص الرسائل السابقة في %{channel_name}"
+ transcript_body: "لمنحك مزيدًا من السياق، فقد ضمَّنا نسخة من الرسائل السابقة في هذه المحادثة (حتى عشر رسائل):\n\n%{transcript}"
+ channel:
+ statuses:
+ read_only: "للقراءة فقط"
+ archived: "مؤرشفة"
+ closed: "مغلقة"
+ open: "مفتوحة"
+ archive:
+ first_post_raw: "هذا الموضوع عبارة عن أرشيف لقناة الدردشة [%{channel_name}] (%{channel_url})."
+ messages_moved:
+ zero: "نقل @%{acting_username} %{count} رسائل إلى القناة [%{channel_name}](%{first_moved_message_url})."
+ one: "نقل @%{acting_username} رسالة واحدة إلى القناة [%{channel_name}](%{first_moved_message_url})."
+ two: "نقل @%{acting_username} رسالتين (%{count}) إلى القناة [%{channel_name}](%{first_moved_message_url})."
+ few: "نقل @%{acting_username} %{count} رسائل إلى القناة [%{channel_name}](%{first_moved_message_url})."
+ many: "نقل @%{acting_username} %{count} رسائل إلى القناة [%{channel_name}](%{first_moved_message_url})."
+ other: "نقل @%{acting_username} %{count} رسائل إلى القناة [%{channel_name}](%{first_moved_message_url})."
+ dm_title:
+ single_user: "%{user}"
+ multi_user: "%{users}"
+ multi_user_truncated: "%{users} و%{leftover} آخرين"
+ bookmarkable:
+ notification_title: "رسالة في %{channel_name}"
+ personal_chat: "الدردشة الشخصية"
+ onebox:
+ inline_to_message: "الرسالة #%{message_id} بواسطة %{username} - #%{chat_channel}"
+ inline_to_channel: "الدردشة #%{chat_channel}"
+ inline_to_topic_channel: "دردشة للموضوع %{topic_title}"
+ x_members:
+ zero: "%{count} عضوًا"
+ one: "عضو واحد (%{count})"
+ two: "عضوان (%{count})"
+ few: "%{count} أعضاء"
+ many: "%{count} عضوًا"
+ other: "%{count} عضوًا"
+ and_x_others:
+ zero: "و%{count} آخرين"
+ one: "و%{count} آخر"
+ two: "و%{count} آخران"
+ few: "و%{count} آخرين"
+ many: "و%{count} آخرين"
+ other: "و%{count} آخرين"
+ discourse_push_notifications:
+ popup:
+ chat_mention:
+ direct: 'أشار %{username} إليك في "%{channel}"'
+ other_type: 'أشار %{username} إلى %{identifier} في "%{channel}"'
+ direct_message_chat_mention:
+ direct: "أشار %{username} إليك في الدردشة الشخصية"
+ other_type: "أشار %{username} إلى %{identifier} في الدردشة الشخصية"
+ new_chat_message: 'أرسل %{username} رسالة في "%{channel}"'
+ new_direct_chat_message: "أرسل %{username} رسالة في الدردشة الشخصية"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ title: إرسال رسالة دردشة
+ reviewable_score_types:
+ needs_review:
+ title: "بحاجة إلى المراجعة"
+ notify_user:
+ chat_pm_title: 'رسالة الدردشة الخاصة بك في "%{channel_name}"'
+ chat_pm_body: "%{link}\n\n%{message}"
+ notify_moderators:
+ chat_pm_title: 'هناك رسالة دردشة في "%{channel_name}" تتطلب انتباه فريق العمل'
+ chat_pm_body: "%{link}\n\n%{message}"
+ reviewables:
+ reasons:
+ chat_message_queued_by_staff: "يعتقد أحد أعضاء فريق العمل أن رسالة الدردشة هذه تحتاج إلى المراجعة."
+ user_notifications:
+ chat_summary:
+ deleted_user: "تم حذف المستخدم"
+ description:
+ zero: "لديك رسائل دردشة جديدة"
+ one: "لديك رسالة دردشة جديدة"
+ two: "لديك رسالتا دردشة جديدتان"
+ few: "لديك رسائل دردشة جديدة"
+ many: "لديك رسائل دردشة جديدة"
+ other: "لديك رسائل دردشة جديدة"
+ from: "%{site_name}"
+ subject:
+ direct_message:
+ zero: "[%{email_prefix}] رسالتان جديدتان من %{message_title} و%{others}"
+ one: "[%{email_prefix}] رسالة جديدة من %{message_title}"
+ two: "[%{email_prefix}] رسالتان جديدتان من %{message_title} و%{others}"
+ few: "[%{email_prefix}] رسالتان جديدتان من %{message_title} و%{others}"
+ many: "[%{email_prefix}] رسالتان جديدتان من %{message_title} و%{others}"
+ other: "[%{email_prefix}] رسالتان جديدتان من %{message_title} و%{others}"
+ chat_channel:
+ zero: "[%{email_prefix}] رسالتان جديدتان في %{message_title} و%{others}"
+ one: "[%{email_prefix}] رسالة جديدة في %{message_title}"
+ two: "[%{email_prefix}] رسالتان جديدتان في %{message_title} و%{others}"
+ few: "[%{email_prefix}] رسائل جديدة في %{message_title} و%{others}"
+ many: "[%{email_prefix}] رسالتان جديدتان في %{message_title} و%{others}"
+ other: "[%{email_prefix}] رسالتان جديدتان في %{message_title} و%{others}"
+ other_direct_message: "من %{message_title}"
+ others: "%{count} آخرين"
+ unsubscribe: "يتم إرسال ملخص الدردشة هذا من %{site_link} عندما تكون غائبًا. غيِّر %{email_preferences_link} أو %{unsubscribe_link} لإلغاء الاشتراك."
+ unsubscribe_no_link: "يتم إرسال ملخص الدردشة هذا من %{site_link} عندما تكون غائبًا. غيِّر %{email_preferences_link} لديك."
+ view_messages:
+ zero: "عرض %{count} رسائل"
+ one: "عرض رسالة واحدة (%{count})"
+ two: "عرض رسالتين (%{count})"
+ few: "عرض %{count} رسائل"
+ many: "عرض %{count} رسائل"
+ other: "عرض %{count} رسائل"
+ view_more:
+ zero: "عرض %{count} رسائل إضافية"
+ one: "عرض رسالة واحدة (%{count}) إضافية"
+ two: "عرض رسالتين (%{count}) إضافيتين"
+ few: "عرض %{count} رسائل إضافية"
+ many: "عرض %{count} رسائل إضافية"
+ other: "عرض %{count} رسائل إضافية"
+ your_chat_settings: "تفضيل مدى تكرار رسائل البريد الإلكتروني للدردشة"
+ unsubscribe:
+ chat_summary:
+ select_title: "حدِّد مدى تكرار الرسائل الإلكترونية لملخص الدردشة:"
+ never: أبدًا
+ when_away: عندما أكون غائبًا فقط
+ category:
+ cannot_delete:
+ has_chat_channels: "لا يمكن حذف هذه الفئة لأنها تحتوي قنوات دردشة."
diff --git a/plugins/chat/config/locales/server.be.yml b/plugins/chat/config/locales/server.be.yml
new file mode 100644
index 0000000000..0be1dcea5e
--- /dev/null
+++ b/plugins/chat/config/locales/server.be.yml
@@ -0,0 +1,36 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+be:
+ chat:
+ deleted_chat_username: аддалены
+ errors:
+ not_accepting_dms: "На жаль, %{username}не прымае паведамленні ў дадзены момант."
+ reviewables:
+ actions:
+ agree:
+ title: "Пагадзіцеся ..."
+ agree_and_suspend:
+ title: "прыпыненне карыстальніка"
+ description: "Пагадзіцеся са сцягамі і падвесіць карыстальнік."
+ agree_and_silence:
+ title: "Silence Карыстальнік"
+ description: "Пагадзіцеся са сцягамі і маўчанне карыстальніка."
+ disagree:
+ title: "не згаджацца"
+ ignore:
+ title: "ігнараваць"
+ channel:
+ statuses:
+ closed: "Закрыта"
+ reviewable_score_types:
+ notify_user:
+ chat_pm_body: "%{link}\n\n%{message}"
+ notify_moderators:
+ chat_pm_body: "%{link}\n\n%{message}"
+ unsubscribe:
+ chat_summary:
+ never: ніколі
diff --git a/plugins/chat/config/locales/server.bg.yml b/plugins/chat/config/locales/server.bg.yml
new file mode 100644
index 0000000000..7f6d844e41
--- /dev/null
+++ b/plugins/chat/config/locales/server.bg.yml
@@ -0,0 +1,33 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+bg:
+ chat:
+ deleted_chat_username: изтрит
+ reviewables:
+ actions:
+ agree_and_suspend:
+ title: "Преустанови потребител"
+ disagree:
+ title: "Не съм съгласен "
+ ignore:
+ title: "Игнорирай"
+ channel:
+ statuses:
+ closed: "Затворена"
+ open: "Отвори"
+ discourse_push_notifications:
+ popup:
+ chat_mention:
+ direct: '%{username} ви спомена в "%{channel}"'
+ reviewable_score_types:
+ notify_user:
+ chat_pm_body: "%{link}\n\n%{message}"
+ notify_moderators:
+ chat_pm_body: "%{link}\n\n%{message}"
+ unsubscribe:
+ chat_summary:
+ never: Никога
diff --git a/plugins/chat/config/locales/server.bs_BA.yml b/plugins/chat/config/locales/server.bs_BA.yml
new file mode 100644
index 0000000000..1060aaa9bc
--- /dev/null
+++ b/plugins/chat/config/locales/server.bs_BA.yml
@@ -0,0 +1,37 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+bs_BA:
+ chat:
+ deleted_chat_username: obrisano
+ reviewables:
+ actions:
+ agree_and_suspend:
+ title: "Suspend User"
+ description: "Složij se sa prijavom i suspendiraj prijavljenog korisnika (suspend)"
+ agree_and_silence:
+ title: "Stišaj korisnika"
+ description: "Složij se sa prijavom i utišaj prijavljenog korisnika (silence)"
+ disagree:
+ title: "Disagree"
+ ignore:
+ title: "Zanemari"
+ channel:
+ statuses:
+ closed: "Zatvoreno"
+ open: "Otvori"
+ discourse_push_notifications:
+ popup:
+ chat_mention:
+ direct: '%{username} vas je spomenuo/la u "%{channel}"'
+ reviewable_score_types:
+ notify_user:
+ chat_pm_body: "%{link}\n\n%{message}"
+ notify_moderators:
+ chat_pm_body: "%{link}\n\n%{message}"
+ unsubscribe:
+ chat_summary:
+ never: Nikad
diff --git a/plugins/chat/config/locales/server.ca.yml b/plugins/chat/config/locales/server.ca.yml
new file mode 100644
index 0000000000..c0e7b4def3
--- /dev/null
+++ b/plugins/chat/config/locales/server.ca.yml
@@ -0,0 +1,44 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+ca:
+ chat:
+ deleted_chat_username: suprimit
+ errors:
+ not_accepting_dms: "De moment %{username} no accepta missatges."
+ reviewables:
+ actions:
+ agree:
+ title: "D'acord..."
+ agree_and_suspend:
+ title: "Suspèn l'usuari"
+ description: "D'acord amb la bandera i suspèn l'usuari. "
+ agree_and_silence:
+ title: "Silencia l'usuari"
+ description: "D'acord amb la bandera i silencia l'usuari. "
+ disagree:
+ title: "En desacord"
+ ignore:
+ title: "Ignora"
+ channel:
+ statuses:
+ closed: "Tancat"
+ open: "Obre"
+ discourse_push_notifications:
+ popup:
+ chat_mention:
+ direct: '%{username} us ha mencionat en "%{channel}"'
+ reviewable_score_types:
+ notify_user:
+ chat_pm_body: "%{link}\n\n%{message}"
+ notify_moderators:
+ chat_pm_body: "%{link}\n\n%{message}"
+ user_notifications:
+ chat_summary:
+ from: "%{site_name}"
+ unsubscribe:
+ chat_summary:
+ never: Mai
diff --git a/plugins/chat/config/locales/server.cs.yml b/plugins/chat/config/locales/server.cs.yml
new file mode 100644
index 0000000000..e81b47dc6d
--- /dev/null
+++ b/plugins/chat/config/locales/server.cs.yml
@@ -0,0 +1,39 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+cs:
+ chat:
+ deleted_chat_username: smazáno
+ errors:
+ not_accepting_dms: "Bohužel, %{username} v současnosti nepřijímá zprávy."
+ reviewables:
+ actions:
+ agree_and_suspend:
+ title: "Zakázat uživatele"
+ description: "Schválit nahlášení a pozastavit uživatele."
+ agree_and_silence:
+ title: "Ztišit uživatele"
+ description: "Schválit nahlášení a ztišit uživatele."
+ disagree:
+ title: "Neschvaluji"
+ ignore:
+ title: "ignorovat"
+ channel:
+ statuses:
+ closed: "Uzavřeno"
+ open: "Otevřít"
+ discourse_push_notifications:
+ popup:
+ chat_mention:
+ direct: 'Uživatel %{username} vás zmínil v "%{channel}"'
+ reviewable_score_types:
+ notify_user:
+ chat_pm_body: "%{link}\\n\\n%{message}\n"
+ notify_moderators:
+ chat_pm_body: "%{link}\\n\\n%{message}\n"
+ unsubscribe:
+ chat_summary:
+ never: Nikdy
diff --git a/plugins/chat/config/locales/server.da.yml b/plugins/chat/config/locales/server.da.yml
new file mode 100644
index 0000000000..961aa543b6
--- /dev/null
+++ b/plugins/chat/config/locales/server.da.yml
@@ -0,0 +1,69 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+da:
+ chat:
+ deleted_chat_username: slettet
+ errors:
+ not_accepting_dms: "Beklager, %{username} accepterer ikke meddelelser i øjeblikket."
+ reviewables:
+ actions:
+ agree:
+ title: "Enig..."
+ agree_and_suspend:
+ title: "Suspendér Bruger"
+ agree_and_silence:
+ title: "Ignorer Bruger"
+ agree_and_restore:
+ title: "Gendan Besked"
+ description: "Gendan meddelelsen, så brugerne kan se den."
+ agree_and_delete:
+ title: "Slet Besked"
+ description: "Slet meddelelsen, så brugerne ikke kan se den."
+ delete_and_agree:
+ title: "Slet Besked"
+ disagree:
+ title: "Uenig"
+ ignore:
+ title: "Ignorér"
+ channel:
+ statuses:
+ read_only: "Kun læsning"
+ archived: "Arkiveret"
+ closed: "Lukket"
+ open: "Åbn"
+ archive:
+ first_post_raw: "Dette emne er et arkiv af chatkanalen [%{channel_name}] (%{channel_url})."
+ bookmarkable:
+ notification_title: "besked i %{channel_name}"
+ personal_chat: "personlig chat"
+ discourse_push_notifications:
+ popup:
+ chat_mention:
+ direct: '%{username} nævnte dig i "%{channel}"'
+ other_type: '%{username} nævnte %{identifier} i "%{channel}"'
+ direct_message_chat_mention:
+ direct: "%{username} nævnte dig i personlig chat"
+ other_type: "%{username} nævnte %{identifier} i personlig chat"
+ new_chat_message: '%{username} sendte en besked i "%{channel}"'
+ new_direct_chat_message: "%{username} sendte en besked i personlig chat"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ title: Send chatbesked
+ reviewable_score_types:
+ needs_review:
+ title: "Behøver Gennemgang"
+ notify_user:
+ chat_pm_body: "%{link}\n\n%{message}"
+ notify_moderators:
+ chat_pm_body: "%{link}\n\n%{message}"
+ user_notifications:
+ chat_summary:
+ from: "%{site_name}"
+ unsubscribe:
+ chat_summary:
+ never: Aldrig
diff --git a/plugins/chat/config/locales/server.de.yml b/plugins/chat/config/locales/server.de.yml
new file mode 100644
index 0000000000..bc885328b5
--- /dev/null
+++ b/plugins/chat/config/locales/server.de.yml
@@ -0,0 +1,184 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+de:
+ site_settings:
+ chat_allowed_groups: "Benutzer in diesen Gruppen können chatten. Bitte beachte, dass Teammitglieder jederzeit auf den Chat zugreifen können."
+ chat_channel_retention_days: "Chat-Nachrichten in regulären Kanälen werden so viele Tage lang aufbewahrt. Auf „0“ setzen, um Nachrichten für immer aufzubewahren."
+ chat_dm_retention_days: "Chat-Nachrichten in persönlichen Chat-Kanälen werden so viele Tage lang aufbewahrt. Auf „0“ setzen, um Nachrichten für immer aufzubewahren."
+ chat_auto_silence_duration: "Anzahl der Minuten, für die Benutzer stummgeschaltet werden, wenn sie das Limit für die Erstellung von Chat-Nachrichten überschreiten. Auf „0“ setzen, um die automatische Stummschaltung zu deaktivieren."
+ chat_allowed_messages_for_trust_level_0: "Anzahl der Nachrichten, die Benutzer mit Vertrauensstufe 0 in 30 Sekunden senden dürfen. Auf „0“ setzen, um das Limit zu deaktivieren."
+ chat_allowed_messages_for_other_trust_levels: "Anzahl der Nachrichten, die Benutzer mit den Vertrauensstufen 1–4 in 30 Sekunden senden dürfen. Auf „0“ setzen, um das Limit zu deaktivieren."
+ chat_silence_user_sensitivity: "Die Wahrscheinlichkeit, dass ein im Chat markierter Benutzer automatisch stummgeschaltet wird."
+ chat_auto_silence_from_flags_duration: "Anzahl der Minuten, für die Benutzer stummgeschaltet werden, wenn sie aufgrund markierter Chat-Nachrichten automatisch stummgeschaltet werden."
+ chat_default_channel_id: "Der Chat-Kanal, der standardmäßig geöffnet wird, wenn ein Benutzer keine ungelesenen Nachrichten oder Erwähnungen in anderen Kanälen hat."
+ chat_duplicate_message_sensitivity: "Die Wahrscheinlichkeit, dass eine doppelte Nachricht desselben Absenders innerhalb eines kurzen Zeitraums blockiert wird. Dezimalzahl zwischen 0 und 1,0, wobei 1,0 die höchste Einstellung ist (Nachrichten werden häufiger in kürzerer Zeit blockiert). Auf `0` setzen, um doppelte Nachrichten zuzulassen."
+ chat_minimum_message_length: "Mindestanzahl von Zeichen für eine Chat-Nachricht."
+ chat_allow_uploads: "Uploads in öffentlichen Chat-Kanälen und Direktnachrichtenkanälen zulassen."
+ chat_archive_destination_topic_status: "Der Status, den das Zielthema haben soll, sobald eine Kanalarchivierung abgeschlossen ist. Dies gilt nur, wenn das Zielthema ein neues Thema ist und kein vorhandenes."
+ default_emoji_reactions: "Standard-Emoji-Reaktionen für Chat-Nachrichten. Füge bis zu 5 Emojis für schnelle Reaktionen hinzu."
+ direct_message_enabled_groups: "Benutzern in diesen Gruppen erlauben, persönliche Benutzer-zu-Benutzer-Chats zu erstellen. Hinweis: Teammitglieder können immer persönliche Chats erstellen und Benutzer können auf persönliche Chats antworten, die von Benutzern initiiert wurden, die die Berechtigung haben, sie zu erstellen."
+ chat_message_flag_allowed_groups: "Benutzer in diesen Gruppen dürfen Chat-Nachrichten markieren."
+ errors:
+ chat_default_channel: "Der Standard-Chat-Kanal muss ein öffentlicher Kanal sein."
+ direct_message_enabled_groups_invalid: "Du musst mindestens eine Gruppe für diese Einstellung angeben. Wenn du nicht möchtest, dass andere Personen als Teammitglieder Direktnachrichten senden, wähle die Gruppe für Teammitglieder aus."
+ chat_upload_not_allowed_secure_uploads: "Chat-Uploads sind nicht erlaubt, wenn die Website-Einstellung für sichere Uploads aktiviert ist."
+ system_messages:
+ chat_channel_archive_complete:
+ title: "Chat-Kanal-Archivierung abgeschlossen"
+ subject_template: "Chat-Kanal-Archivierung erfolgreich abgeschlossen"
+ text_body_template: |
+ Die Archivierung des Chat-Kanals **\#%{channel_name}** wurde erfolgreich abgeschlossen. Die Nachrichten wurden in das Thema [%{topic_title}](%{topic_url}) kopiert.
+ chat_channel_archive_failed:
+ title: "Chat-Kanal-Archivierung fehlgeschlagen"
+ subject_template: "Chat-Kanal-Archivierung fehlgeschlagen"
+ text_body_template: |
+ Die Archivierung des Chat-Kanals **\#%{channel_name}** ist fehlgeschlagen. %{messages_archived} Nachrichten wurden archiviert. Teilweise archivierte Nachrichten wurden in das Thema [%{topic_title}](%{topic_url}) kopiert. Besuche den Kanal unter %{channel_url}, um es erneut zu versuchen.
+ chat:
+ deleted_chat_username: gelöscht
+ errors:
+ channel_exists_for_category: "Für diese Kategorie und diesen Namen existiert bereits ein Kanal"
+ channel_new_message_disallowed: "Der Kanal ist %{status}, es können keine neuen Nachrichten gesendet werden"
+ channel_modify_message_disallowed: "Der Kanal ist %{status}, es können keine Nachrichten bearbeitet oder gelöscht werden"
+ user_cannot_send_message: "Du kannst derzeit keine Nachrichten senden."
+ rate_limit_exceeded: "Das Limit der Chat-Nachrichten, die innerhalb von 30 Sekunden gesendet werden können, wurde überschritten"
+ auto_silence_from_flags: "Chat-Nachricht wurde mit einer Punktzahl markiert, die hoch genug ist, um den Benutzer stummzuschalten."
+ channel_cannot_be_archived: "Der Kanal kann zu diesem Zeitpunkt nicht archiviert werden, er muss entweder geschlossen oder geöffnet sein."
+ duplicate_message: "Du hast vor Kurzem eine identische Nachricht gepostet."
+ delete_channel_failed: "Löschen des Kanals fehlgeschlagen, bitte versuche es erneut."
+ minimum_length_not_met: "Die Nachricht ist zu kurz, sie muss mindestens %{minimum} Zeichen lang sein."
+ max_reactions_limit_reached: "Neue Reaktionen auf diese Nachricht sind nicht erlaubt."
+ message_move_invalid_channel: "Quell- und Zielkanal müssen öffentliche Kanäle sein."
+ message_move_no_messages_found: "Es wurden keine Nachrichten mit den angegebenen Nachrichten-IDs gefunden."
+ cant_update_direct_message_channel: "Eigenschaften des Direktnachrichtenkanals wie Name und Beschreibung können nicht aktualisiert werden."
+ not_accepting_dms: "%{username} akzeptiert derzeit leider keine Nachrichten."
+ actor_ignoring_target_user: "Du ignorierst %{username}, daher kannst du keine Nachrichten an die Person senden."
+ actor_muting_target_user: "Du hast %{username} stummgeschaltet, daher kannst du keine Nachrichten an die Person senden."
+ actor_disallowed_dms: "Du hast dich dafür entschieden, dass Benutzer dir keine privaten und Direktnachrichten schicken können, daher kannst du keine neuen Direktnachrichten erstellen."
+ actor_preventing_target_user_from_dm: "Du hast dich dafür entschieden, dass %{username} dir keine privaten und Direktnachrichten schicken kann, daher kannst du keine neuen Direktnachrichten an diese Person erstellen."
+ user_cannot_send_direct_messages: "Du kannst leider keine Direktnachrichten senden."
+ reviewables:
+ message_already_handled: "Danke, aber wir haben diese Nachricht bereits überprüft und festgestellt, dass sie nicht erneut markiert werden muss."
+ actions:
+ agree:
+ title: "Zustimmen …"
+ agree_and_keep_message:
+ title: "Nachricht behalten"
+ description: "Markierung zustimmen und die Nachricht unverändert beibehalten."
+ agree_and_keep_deleted:
+ title: "Nachricht gelöscht lassen"
+ description: "Markierung zustimmen und die Nachricht gelöscht lassen."
+ agree_and_suspend:
+ title: "Benutzer sperren"
+ description: "Markierung zustimmen und den Benutzer sperren."
+ agree_and_silence:
+ title: "Benutzer stummschalten"
+ description: "Markierung zustimmen und den Benutzer stummschalten."
+ agree_and_restore:
+ title: "Nachricht wiederherstellen"
+ description: "Nachricht wiederherstellen, damit Benutzer sie sehen können."
+ agree_and_delete:
+ title: "Nachricht löschen"
+ description: "Nachricht löschen, damit Benutzer sie nicht sehen können."
+ delete_and_agree:
+ title: "Nachricht löschen"
+ disagree_and_restore:
+ title: "Ablehnen und Nachricht wiederherstellen"
+ description: "Nachricht wiederherstellen, damit alle Benutzer sie sehen können."
+ disagree:
+ title: "Ablehnen"
+ ignore:
+ title: "Ignorieren"
+ direct_messages:
+ transcript_title: "Transkript früherer Nachrichten in %{channel_name}"
+ transcript_body: "Um dir mehr Kontext zu geben, haben wir ein Transkript der vorherigen Nachrichten in dieser Unterhaltung beigefügt (bis zu zehn):\n\n%{transcript}"
+ channel:
+ statuses:
+ read_only: "Schreibgeschützt"
+ archived: "Archiviert"
+ closed: "Geschlossen"
+ open: "Offen"
+ archive:
+ first_post_raw: "Dieses Thema ist ein Archiv des Chat-Kanals [%{channel_name}](%{channel_url})."
+ messages_moved:
+ one: "@%{acting_username} hat eine Nachricht in den Kanal [%{channel_name}](%{first_moved_message_url}) verschoben."
+ other: "@%{acting_username} hat %{count} Nachrichten in den Kanal [%{channel_name}](%{first_moved_message_url}) verschoben."
+ dm_title:
+ single_user: "%{user}"
+ multi_user: "%{users}"
+ multi_user_truncated: "%{users} und %{leftover} andere"
+ bookmarkable:
+ notification_title: "Nachricht in %{channel_name}"
+ personal_chat: "persönlicher Chat"
+ onebox:
+ inline_to_message: "Nachricht #%{message_id} von %{username} – #%{chat_channel}"
+ inline_to_channel: "Chat #%{chat_channel}"
+ inline_to_topic_channel: "Chat für Thema %{topic_title}"
+ x_members:
+ one: "%{count} Mitglied"
+ other: "%{count} Mitglieder"
+ and_x_others:
+ one: "und %{count} andere Person"
+ other: "und %{count} andere"
+ discourse_push_notifications:
+ popup:
+ chat_mention:
+ direct: '%{username} hat dich in „%{channel}“ erwähnt'
+ other_type: '%{username} hat %{identifier} in „%{channel}“ erwähnt'
+ direct_message_chat_mention:
+ direct: "%{username} hat dich im persönlichen Chat erwähnt"
+ other_type: "%{username} hat %{identifier} im persönlichen Chat erwähnt"
+ new_chat_message: '%{username} hat eine Nachricht in „%{channel}“ gesendet'
+ new_direct_chat_message: "%{username} hat eine Nachricht im persönlichen Chat gesendet"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ title: Chat-Nachricht senden
+ reviewable_score_types:
+ needs_review:
+ title: "Überprüfung erforderlich"
+ notify_user:
+ chat_pm_title: 'Deine Chat-Nachricht in „%{channel_name}“'
+ chat_pm_body: "%{link}\n\n%{message}"
+ notify_moderators:
+ chat_pm_title: 'Eine Chat-Nachricht in „%{channel_name}“ erfordert die Aufmerksamkeit des Teams'
+ chat_pm_body: "%{link}\n\n%{message}"
+ reviewables:
+ reasons:
+ chat_message_queued_by_staff: "Ein Teammitglied ist der Meinung, dass diese Chat-Nachricht überprüft werden muss."
+ user_notifications:
+ chat_summary:
+ deleted_user: "Gelöschter Benutzer"
+ description:
+ one: "Du hast eine neue Chat-Nachricht"
+ other: "Du hast neue Chat-Nachrichten"
+ from: "%{site_name}"
+ subject:
+ direct_message:
+ one: "[%{email_prefix}] Neue Nachricht von %{message_title}"
+ other: "[%{email_prefix}] Neue Nachrichten von %{message_title} und %{others}"
+ chat_channel:
+ one: "[%{email_prefix}] Neue Nachricht in %{message_title}"
+ other: "[%{email_prefix}] Neue Nachrichten in %{message_title} und %{others}"
+ other_direct_message: "von %{message_title}"
+ others: "%{count} andere"
+ unsubscribe: "Diese Chat-Zusammenfassung wird dir von %{site_link} gesendet, wenn du abwesend bist. Ändere deine %{email_preferences_link} oder %{unsubscribe_link}, um dich abzumelden."
+ unsubscribe_no_link: "Diese Chat-Zusammenfassung wird dir von %{site_link} gesendet, wenn du abwesend bist. Ändere deine %{email_preferences_link}."
+ view_messages:
+ one: "Nachricht anzeigen"
+ other: "%{count} Nachrichten anzeigen"
+ view_more:
+ one: "%{count} weitere Nachricht anzeigen"
+ other: "%{count} weitere Nachrichten anzeigen"
+ your_chat_settings: "Chat-E-Mail-Häufigkeitspräferenz"
+ unsubscribe:
+ chat_summary:
+ select_title: "Häufigkeit von Chat-Zusammenfassungs-E-Mails festlegen auf:"
+ never: Niemals
+ when_away: Nur bei Abwesenheit
+ category:
+ cannot_delete:
+ has_chat_channels: "Diese Kategorie kann nicht gelöscht werden, da sie Chat-Kanäle hat."
diff --git a/plugins/chat/config/locales/server.el.yml b/plugins/chat/config/locales/server.el.yml
new file mode 100644
index 0000000000..8f772e805f
--- /dev/null
+++ b/plugins/chat/config/locales/server.el.yml
@@ -0,0 +1,37 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+el:
+ chat:
+ deleted_chat_username: διεγράφη
+ errors:
+ not_accepting_dms: "Λυπούμαστε, ο/η %{username} δεν δέχεται μηνύματα αυτή τη στιγμή."
+ reviewables:
+ actions:
+ agree_and_suspend:
+ title: "Αποβολή Χρήστη"
+ agree_and_silence:
+ title: "Σίγαση Χρήστη"
+ disagree:
+ title: "Διαφωνώ"
+ ignore:
+ title: "Αγνόηση"
+ channel:
+ statuses:
+ closed: "Κλειστό"
+ open: "Ξεκίνημα"
+ discourse_push_notifications:
+ popup:
+ chat_mention:
+ direct: '%{username} σε ανέφερε στο "%{channel}"'
+ reviewable_score_types:
+ notify_user:
+ chat_pm_body: "%{link}\n\n%{message}"
+ notify_moderators:
+ chat_pm_body: "%{link}\n\n%{message}"
+ unsubscribe:
+ chat_summary:
+ never: Ποτέ
diff --git a/plugins/chat/config/locales/server.en.yml b/plugins/chat/config/locales/server.en.yml
new file mode 100644
index 0000000000..ea2349f2b7
--- /dev/null
+++ b/plugins/chat/config/locales/server.en.yml
@@ -0,0 +1,196 @@
+en:
+ site_settings:
+ chat_enabled: "Enable the chat plugin."
+ chat_allowed_groups: "Users in these groups can chat. Note that staff can always access chat."
+ chat_channel_retention_days: "Chat messages in regular channels will be retained for this many days. Set to '0' to retain messages forever."
+ chat_dm_retention_days: "Chat messages in personal chat channels will be retained for this many days. Set to '0' to retain messages forever."
+ chat_auto_silence_duration: "Number of minutes that users will be silenced for when they exceed the chat message creation rate limit. Set to '0' to disable auto-silencing."
+ chat_allowed_messages_for_trust_level_0: "Number of messages that trust level 0 users is allowed to send in 30 seconds. Set to '0' to disable limit."
+ chat_allowed_messages_for_other_trust_levels: "Number of messages that users with trust levels 1-4 is allowed to send in 30 seconds. Set to '0' to disable limit."
+ chat_silence_user_sensitivity: "The likelihood that a user flagged in chat will be automatically silenced."
+ chat_auto_silence_from_flags_duration: "Number of minutes that users will be silenced for when they are automatically silenced due to flagged chat messages."
+ chat_default_channel_id: "The chat channel that will be opened by default when a user has no unread messages or mentions in other channels."
+ chat_duplicate_message_sensitivity: "The likelihood that a duplicate message by the same sender will be blocked in a short period. Decimal number between 0 and 1.0, with 1.0 being the highest setting (blocks messages more frequently in a shorter amount of time). Set to `0` to allow duplicate messages."
+ chat_minimum_message_length: "Minimum number of characters for a chat message."
+ chat_allow_uploads: "Allow uploads in public chat channels and direct message channels."
+ chat_archive_destination_topic_status: "The status that the destination topic should be once a channel archive is completed. This only applies when the destination topic is a new topic, not an existing one."
+ default_emoji_reactions: "Default emoji reactions for chat messages. Add up to 5 emojis for quick reaction."
+ direct_message_enabled_groups: "Allow users within these groups to create user-to-user Personal Chats. Note: staff can always create Personal Chats, and users will be able to reply to Personal Chats initiated by users who have permission to create them."
+ chat_message_flag_allowed_groups: "Users in these groups are allowed to flag chat messages."
+ errors:
+ chat_default_channel: "The default chat channel must be a public channel."
+ direct_message_enabled_groups_invalid: "You must specify at least one group for this setting. If you do not want anyone except staff to send direct messages, choose the staff group."
+ chat_upload_not_allowed_secure_uploads: "Chat uploads are not allowed when secure uploads site setting is enabled."
+ system_messages:
+ chat_channel_archive_complete:
+ title: "Chat Channel Archive Complete"
+ subject_template: "Chat channel archive completed successfully"
+ text_body_template: |
+ Archiving the chat channel **\#%{channel_name}** has been completed successfully. The messages were copied into the topic [%{topic_title}](%{topic_url}).
+ chat_channel_archive_failed:
+ title: "Chat Channel Archive Failed"
+ subject_template: "Chat channel archive failed"
+ text_body_template: |
+ Archiving the chat channel **\#%{channel_name}** has failed. %{messages_archived} messages have been archived. Partially archived messages were copied into the topic [%{topic_title}](%{topic_url}). Visit the channel at %{channel_url} to retry.
+
+ chat:
+ deleted_chat_username: deleted
+ errors:
+ channel_exists_for_category: "A channel already exists for this category and name"
+ channel_new_message_disallowed: "The channel is %{status}, no new messages can be sent"
+ channel_modify_message_disallowed: "The channel is %{status}, no messages can be edited or deleted"
+ user_cannot_send_message: "You cannot send messages at this time."
+ rate_limit_exceeded: "Exceeded the limit of chat messages that can be sent within 30 seconds"
+ auto_silence_from_flags: "Chat message flagged with score high enough to silence user."
+ channel_cannot_be_archived: "The channel cannot be archived at this time, it must be either closed or open to archive."
+ duplicate_message: "You posted an identical message too recently."
+ delete_channel_failed: "Delete channel failed, please try again."
+ minimum_length_not_met: "Message is too short, must have a minimum of %{minimum} characters."
+ max_reactions_limit_reached: "New reactions are not allowed on this message."
+ message_move_invalid_channel: "The source and destination channel must be public channels."
+ message_move_no_messages_found: "No messages were found with the provided message IDs."
+ cant_update_direct_message_channel: "Direct message channel properties like name and description can’t be updated."
+ not_accepting_dms: "Sorry, %{username} is not accepting messages at the moment."
+ actor_ignoring_target_user: "You are ignoring %{username}, so you cannot send messages to them."
+ actor_muting_target_user: "You are muting %{username}, so you cannot send messages to them."
+ actor_disallowed_dms: "You have chosen to prevent users from sending you private and direct messages, so you cannot create new direct messages."
+ actor_preventing_target_user_from_dm: "You have chosen to prevent %{username} from sending you private and direct messages, so you cannot create new direct messages to them."
+ user_cannot_send_direct_messages: "Sorry, you cannot send direct messages."
+ reviewables:
+ message_already_handled: "Thanks, but we've already reviewed this message and determined it does not need to be flagged again."
+ actions:
+ agree:
+ title: "Agree..."
+ agree_and_keep_message:
+ title: "Keep Message"
+ description: "Agree with flag and keep the message unchanged."
+ agree_and_keep_deleted:
+ title: "Keep Message Deleted"
+ description: "Agree with flag and leave the message deleted."
+ agree_and_suspend:
+ title: "Suspend User"
+ description: "Agree with flag and suspend the user."
+ agree_and_silence:
+ title: "Silence User"
+ description: "Agree with flag and silence the user."
+ agree_and_restore:
+ title: "Restore Message"
+ description: "Restore the message so that users can see it."
+ agree_and_delete:
+ title: "Delete Message"
+ description: "Delete the message so that users cannot see it."
+ delete_and_agree:
+ title: "Delete Message"
+ disagree_and_restore:
+ title: "Disagree and Restore Message"
+ description: "Restore the message so that all users can see it."
+ disagree:
+ title: "Disagree"
+ ignore:
+ title: "Ignore"
+ direct_messages:
+ transcript_title: "Transcript of previous messages in %{channel_name}"
+ transcript_body: "To give you more context, we included a transcript of the previous messages in this conversation (up to ten):\n\n%{transcript}"
+ channel:
+ statuses:
+ read_only: "Read Only"
+ archived: "Archived"
+ closed: "Closed"
+ open: "Open"
+ archive:
+ first_post_raw: "This topic is an archive of the [%{channel_name}](%{channel_url}) chat channel."
+ messages_moved:
+ one: "@%{acting_username} moved a message to the [%{channel_name}](%{first_moved_message_url}) channel."
+ other: "@%{acting_username} moved %{count} messages to the [%{channel_name}](%{first_moved_message_url}) channel."
+ dm_title:
+ single_user: "%{user}"
+ multi_user: "%{users}"
+ multi_user_truncated: "%{users} and %{leftover} others"
+
+ category_channel:
+ errors:
+ slug_contains_non_ascii_chars: "contains non-ascii characters"
+ is_already_in_use: "is already in use"
+
+ bookmarkable:
+ notification_title: "message in %{channel_name}"
+
+ personal_chat: "personal chat"
+
+ onebox:
+ inline_to_message: "Message #%{message_id} by %{username} – #%{chat_channel}"
+ inline_to_channel: "Chat #%{chat_channel}"
+ inline_to_topic_channel: "Chat for Topic %{topic_title}"
+
+ x_members:
+ one: "%{count} member"
+ other: "%{count} members"
+
+ and_x_others:
+ one: "and %{count} other"
+ other: "and %{count} others"
+
+ discourse_push_notifications:
+ popup:
+ chat_mention:
+ direct: '%{username} mentioned you in "%{channel}"'
+ other_type: '%{username} mentioned %{identifier} in "%{channel}"'
+ direct_message_chat_mention:
+ direct: "%{username} mentioned you in personal chat"
+ other_type: "%{username} mentioned %{identifier} in personal chat"
+ new_chat_message: '%{username} sent a message in "%{channel}"'
+ new_direct_chat_message: "%{username} sent a message in personal chat"
+
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ title: Send chat message
+
+ reviewable_score_types:
+ needs_review:
+ title: "Needs Review"
+ notify_user:
+ chat_pm_title: 'Your chat message in "%{channel_name}"'
+ chat_pm_body: "%{link}\n\n%{message}"
+ notify_moderators:
+ chat_pm_title: 'A chat message in "%{channel_name}" requires staff attention'
+ chat_pm_body: "%{link}\n\n%{message}"
+
+ reviewables:
+ reasons:
+ chat_message_queued_by_staff: "A staff member thinks this chat message needs review."
+ user_notifications:
+ chat_summary:
+ deleted_user: "Deleted user"
+ description:
+ one: "You have a new chat message"
+ other: "You have new chat messages"
+ from: "%{site_name}"
+ subject:
+ direct_message:
+ one: "[%{email_prefix}] New message from %{message_title}"
+ other: "[%{email_prefix}] New messages from %{message_title} and %{others}"
+ chat_channel:
+ one: "[%{email_prefix}] New message in %{message_title}"
+ other: "[%{email_prefix}] New messages in %{message_title} and %{others}"
+ other_direct_message: "from %{message_title}"
+ others: "%{count} others"
+ unsubscribe: "This chat summary is sent from %{site_link} when you are away. Change your %{email_preferences_link}, or %{unsubscribe_link} to unsubscribe."
+ unsubscribe_no_link: "This chat summary is sent from %{site_link} when you are away. Change your %{email_preferences_link}."
+ view_messages:
+ one: "View message"
+ other: "View %{count} messages"
+ view_more:
+ one: "View %{count} more message"
+ other: "View %{count} more messages"
+ your_chat_settings: "chat email frequency preference"
+
+ unsubscribe:
+ chat_summary:
+ select_title: "Set chat summary emails frequency to:"
+ never: Never
+ when_away: Only when away
+
+ category:
+ cannot_delete:
+ has_chat_channels: "Can't delete this category because it has chat channels."
diff --git a/plugins/chat/config/locales/server.en_GB.yml b/plugins/chat/config/locales/server.en_GB.yml
new file mode 100644
index 0000000000..2d4fa180ec
--- /dev/null
+++ b/plugins/chat/config/locales/server.en_GB.yml
@@ -0,0 +1,7 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+en_GB:
diff --git a/plugins/chat/config/locales/server.es.yml b/plugins/chat/config/locales/server.es.yml
new file mode 100644
index 0000000000..69c7e21729
--- /dev/null
+++ b/plugins/chat/config/locales/server.es.yml
@@ -0,0 +1,184 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+es:
+ site_settings:
+ chat_allowed_groups: "Los usuarios de estos grupos pueden chatear. Ten en cuenta que el personal siempre puede acceder al chat."
+ chat_channel_retention_days: "Los mensajes del chat en los canales regulares se conservarán durante este número de días. Poner a «0» para retener los mensajes para siempre."
+ chat_dm_retention_days: "Los mensajes de chat en los canales de chat personales se conservarán durante este número de días. Ponlo en «0» para retener los mensajes para siempre."
+ chat_auto_silence_duration: "Número de minutos que los usuarios serán silenciados cuando superen el límite de creación de mensajes de chat. Ponlo en «0» para desactivar el silenciamiento automático."
+ chat_allowed_messages_for_trust_level_0: "Número de mensajes que los usuarios de nivel de confianza 0 pueden enviar en 30 segundos. Ponlo en «0» para desactivar el límite."
+ chat_allowed_messages_for_other_trust_levels: "Número de mensajes que los usuarios con niveles de confianza 1-4 pueden enviar en 30 segundos. Ponlo en «0» para desactivar el límite."
+ chat_silence_user_sensitivity: "La probabilidad de que un usuario denunciado en el chat sea silenciado automáticamente."
+ chat_auto_silence_from_flags_duration: "Número de minutos que los usuarios serán silenciados cuando sean silenciados automáticamente debido a mensajes de chat denunciados."
+ chat_default_channel_id: "El canal de chat que se abrirá por defecto cuando un usuario no tenga mensajes no leídos o menciones en otros canales."
+ chat_duplicate_message_sensitivity: "La probabilidad de que un mensaje duplicado por el mismo remitente sea bloqueado en un corto periodo de tiempo. Número decimal entre 0 y 1,0, siendo 1,0 el ajuste más alto (bloquea los mensajes con más frecuencia en un periodo de tiempo más corto). Ponlo en «0» para permitir los mensajes duplicados."
+ chat_minimum_message_length: "Número mínimo de caracteres para un mensaje de chat."
+ chat_allow_uploads: "Permitir las subidas en los canales de chat públicos y en los canales de mensajes directos."
+ chat_archive_destination_topic_status: "El estado que debe tener el tema de destino una vez completado el archivo de un canal. Esto solo se aplica cuando el tema de destino es un tema nuevo, no uno existente."
+ default_emoji_reactions: "Reacciones emoji por defecto para los mensajes de chat. Añade hasta 5 emojis para una reacción rápida."
+ direct_message_enabled_groups: "Permite a los usuarios de estos grupos crear Chats Personales de usuario a usuario. Nota: el personal siempre puede crear Chats Personales, y los usuarios podrán responder a los Chats Personales iniciados por los usuarios que tienen permiso para crearlos."
+ chat_message_flag_allowed_groups: "Los usuarios de estos grupos pueden marcar los mensajes del chat."
+ errors:
+ chat_default_channel: "El canal de chat por defecto debe ser un canal público."
+ direct_message_enabled_groups_invalid: "Debes especificar al menos un grupo para esta configuración. Si no quieres que nadie, excepto el personal, envíe mensajes directos, elige el grupo del personal."
+ chat_upload_not_allowed_secure_uploads: "Las subidas por chat no están permitidas cuando la configuración del sitio de subidas seguras está activada."
+ system_messages:
+ chat_channel_archive_complete:
+ title: "Archivado del canal de chat completado"
+ subject_template: "El archivado del canal de chat se ha completado con éxito"
+ text_body_template: |
+ El archivado del canal de chat **\#%{channel_name}** se completó con éxito. Los mensajes se copiaron en el tema [%{topic_title}](%{topic_url}).
+ chat_channel_archive_failed:
+ title: "No se pudo archivar el canal"
+ subject_template: "No se pudo archivar el canal"
+ text_body_template: |
+ El archivo del canal de chat **\#%{channel_name}** ha fallado. Se han archivado los mensajes de %{messages_archived}. Los mensajes parcialmente archivados se han copiado en el tema [%{topic_title}](%{topic_url}). Visita el canal en %{channel_url} para volver a intentarlo.
+ chat:
+ deleted_chat_username: eliminado
+ errors:
+ channel_exists_for_category: "Ya existe un canal para esta categoría y nombre"
+ channel_new_message_disallowed: "El canal es %{status}, no se pueden enviar nuevos mensajes"
+ channel_modify_message_disallowed: "El canal está %{status}, no se pueden editar ni eliminar mensajes"
+ user_cannot_send_message: "No puedes enviar mensajes en este momento."
+ rate_limit_exceeded: "Se ha superado el límite de mensajes de chat que se pueden enviar en 30 segundos"
+ auto_silence_from_flags: "Mensaje de chat marcado con una puntuación lo suficientemente alta como para silenciar al usuario."
+ channel_cannot_be_archived: "El canal no se puede archivar en este momento, debe estar cerrado o abierto para ser archivado."
+ duplicate_message: "Tú también publicaste un mensaje idéntico hace poco."
+ delete_channel_failed: "No se pudo eliminar el canal, inténtalo de nuevo."
+ minimum_length_not_met: "El mensaje es demasiado corto, debe tener un mínimo de %{minimum} caracteres."
+ max_reactions_limit_reached: "No se permiten nuevas reacciones en este mensaje."
+ message_move_invalid_channel: "El canal de origen y el de destino deben ser canales públicos."
+ message_move_no_messages_found: "No se ha encontrado ningún mensaje con los identificadores de mensaje proporcionados."
+ cant_update_direct_message_channel: "Las propiedades del canal de mensajes directos, como el nombre y la descripción, no se pueden actualizar."
+ not_accepting_dms: "Lo siento, %{username} no acepta mensajes en este momento."
+ actor_ignoring_target_user: "Estás ignorando a %{username}, por lo que no puedes enviarle mensajes."
+ actor_muting_target_user: "Estás silenciando a %{username}, por lo que no puedes enviarle mensajes."
+ actor_disallowed_dms: "Has elegido impedir que los usuarios te envíen mensajes privados y directos, por lo que no puedes crear nuevos mensajes directos."
+ actor_preventing_target_user_from_dm: "Has elegido impedir que %{username} te envíe mensajes privados y directos, por lo que no puedes crear nuevos mensajes directos para ellos."
+ user_cannot_send_direct_messages: "Lo sentimos, no puedes enviar mensajes directos."
+ reviewables:
+ message_already_handled: "Gracias, pero ya hemos revisado este mensaje y hemos determinado que no es necesario marcarlo de nuevo."
+ actions:
+ agree:
+ title: "De acuerdo..."
+ agree_and_keep_message:
+ title: "Conservar mensaje"
+ description: "Aceptar la denuncia y conservar el mensaje sin cambios."
+ agree_and_keep_deleted:
+ title: "Conservar el mensaje eliminado"
+ description: "Aceptar la denuncia y dejar el mensaje eliminado."
+ agree_and_suspend:
+ title: "Suspender al usuario"
+ description: "Aceptar la denuncia y suspender al usuario."
+ agree_and_silence:
+ title: "Silenciar al usuario"
+ description: "Aceptar la denuncia y silenciar al usuario."
+ agree_and_restore:
+ title: "Restaurar mensaje"
+ description: "Restaura el mensaje para que los usuarios puedan verlo."
+ agree_and_delete:
+ title: "Eliminar mensaje"
+ description: "Elimina el mensaje para que los usuarios no puedan verlo."
+ delete_and_agree:
+ title: "Eliminar mensaje"
+ disagree_and_restore:
+ title: "No aceptar y restaurar el mensaje"
+ description: "Restaura el mensaje para que todos los usuarios puedan verlo."
+ disagree:
+ title: "No estoy de acuerdo"
+ ignore:
+ title: "Ignorar"
+ direct_messages:
+ transcript_title: "Transcripción de los mensajes anteriores en %{channel_name}"
+ transcript_body: "Para darte más contexto, incluimos una transcripción de los mensajes anteriores de esta conversación (hasta diez):\n\n%{transcript}"
+ channel:
+ statuses:
+ read_only: "Solo lectura"
+ archived: "Archivado"
+ closed: "Cerrado"
+ open: "Abierto"
+ archive:
+ first_post_raw: "Este tema es un archivo del canal de chat de [%{channel_name}](%{channel_url})."
+ messages_moved:
+ one: "@%{acting_username} movió un mensaje al canal [%{channel_name}](%{first_moved_message_url})."
+ other: "@%{acting_username} movió %{count} mensajes al canal [%{channel_name}](%{first_moved_message_url})."
+ dm_title:
+ single_user: "%{user}"
+ multi_user: "%{users}"
+ multi_user_truncated: "%{users} y %{leftover} otros"
+ bookmarkable:
+ notification_title: "mensaje en %{channel_name}"
+ personal_chat: "chat personal"
+ onebox:
+ inline_to_message: "Mensaje #%{message_id} de %{username} - #%{chat_channel}"
+ inline_to_channel: "Chat #%{chat_channel}"
+ inline_to_topic_channel: "Chat para el tema %{topic_title}"
+ x_members:
+ one: "%{count} miembro"
+ other: "%{count} miembros"
+ and_x_others:
+ one: "y %{count} otros"
+ other: "y %{count} otros"
+ discourse_push_notifications:
+ popup:
+ chat_mention:
+ direct: '%{username} te mencionó en «%{channel}»'
+ other_type: '%{username} mencionó %{identifier} en «%{channel}»'
+ direct_message_chat_mention:
+ direct: "%{username} te mencionó en el chat personal"
+ other_type: "%{username} mencionó %{identifier} en el chat personal"
+ new_chat_message: '%{username} envió un mensaje en «%{channel}»'
+ new_direct_chat_message: "%{username} envió un mensaje en el chat personal"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ title: Enviar mensaje de chat
+ reviewable_score_types:
+ needs_review:
+ title: "Necesita revisión"
+ notify_user:
+ chat_pm_title: 'Tu mensaje de chat en «%{channel_name}»'
+ chat_pm_body: "%{link}\n\n%{message}"
+ notify_moderators:
+ chat_pm_title: 'Un mensaje de chat en «%{channel_name}» requiere atención del personal'
+ chat_pm_body: "%{link}\n\n%{message}"
+ reviewables:
+ reasons:
+ chat_message_queued_by_staff: "Un miembro del personal cree que este mensaje de chat necesita revisión."
+ user_notifications:
+ chat_summary:
+ deleted_user: "Usuario eliminado"
+ description:
+ one: "Tienes un nuevo mensaje de chat"
+ other: "Tienes nuevos mensajes de chat"
+ from: "%{site_name}"
+ subject:
+ direct_message:
+ one: "[%{email_prefix}] Nuevo mensaje de %{message_title}"
+ other: "[%{email_prefix}] Nuevos mensajes de %{message_title} y %{others}"
+ chat_channel:
+ one: "[%{email_prefix}] Nuevo mensaje en %{message_title}"
+ other: "[%{email_prefix}] Nuevos mensajes en %{message_title} y %{others}"
+ other_direct_message: "de %{message_title}"
+ others: "%{count} otros"
+ unsubscribe: "Este resumen del chat se envía desde %{site_link} cuando estás fuera. Cambia tu %{email_preferences_link}, o %{unsubscribe_link} para darte de baja."
+ unsubscribe_no_link: "Este resumen del chat se envía desde %{site_link} cuando estás fuera. Cambia tu %{email_preferences_link}."
+ view_messages:
+ one: "Ver mensaje"
+ other: "Ver %{count} mensajes"
+ view_more:
+ one: "Ver %{count} mensaje más"
+ other: "Ver %{count} mensajes más"
+ your_chat_settings: "preferencia de la frecuencia del correo electrónico del chat"
+ unsubscribe:
+ chat_summary:
+ select_title: "Establece la frecuencia de los correos electrónicos de resumen del chat:"
+ never: Nunca
+ when_away: Solo cuando estés ausente
+ category:
+ cannot_delete:
+ has_chat_channels: "No se puede eliminar esta categoría porque tiene canales de chat."
diff --git a/plugins/chat/config/locales/server.et.yml b/plugins/chat/config/locales/server.et.yml
new file mode 100644
index 0000000000..432df77dfc
--- /dev/null
+++ b/plugins/chat/config/locales/server.et.yml
@@ -0,0 +1,37 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+et:
+ chat:
+ deleted_chat_username: kustutatud
+ errors:
+ not_accepting_dms: "Kahjuks ei aktsepteeri %{username} hetkel sõnumeid."
+ reviewables:
+ actions:
+ agree_and_suspend:
+ title: "Peata kasutaja"
+ agree_and_silence:
+ title: "Vaigista kasutaja"
+ disagree:
+ title: "Ei nõustu"
+ ignore:
+ title: "Ignoreeri"
+ channel:
+ statuses:
+ closed: "Suletud"
+ open: "Ava"
+ discourse_push_notifications:
+ popup:
+ chat_mention:
+ direct: '%{username} mainis Sind teemas "%{channel}"'
+ reviewable_score_types:
+ notify_user:
+ chat_pm_body: "%{link}\n\n%{message}"
+ notify_moderators:
+ chat_pm_body: "%{link}\n\n%{message}"
+ unsubscribe:
+ chat_summary:
+ never: Mitte kunagi
diff --git a/plugins/chat/config/locales/server.fa_IR.yml b/plugins/chat/config/locales/server.fa_IR.yml
new file mode 100644
index 0000000000..7bdced30a9
--- /dev/null
+++ b/plugins/chat/config/locales/server.fa_IR.yml
@@ -0,0 +1,108 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+fa_IR:
+ site_settings:
+ chat_allowed_groups: "کاربران در این گروهها میتوانند گفتگو کنند. توجه داشته باشید که کارکنان همیشه میتوانند به گفتگو دسترسی داشته باشند."
+ chat_allow_uploads: "بارگذاری در کانالهای گفتگو عمومی و کانالهای پیام مستقیم مجاز است."
+ default_emoji_reactions: "واکنشهای شکلک پیشفرض برای پیامهای گفتگو. برای واکنش سریع تا ۵ شکلک اضافه کنید."
+ chat_message_flag_allowed_groups: "کاربران در این گروهها مجاز به گزارش دادن، پیامهای گفتگو هستند."
+ errors:
+ chat_upload_not_allowed_secure_uploads: "وقتی که در تنظیمات سایت آپلودهای ایمن فعال باشد، آپلود گفتگو مجاز نیست."
+ chat:
+ deleted_chat_username: حذف شد
+ errors:
+ channel_exists_for_category: "یک کانال دیگر از قبل برای این دستهبندی و نام وجود دارد"
+ cant_update_direct_message_channel: "ویژگی پیام مستقیم کانال مانند نام و توضیحات را نمیتوان بهروز کرد."
+ not_accepting_dms: "با عرض پوزش، کاربر %{username} در حال حاضر پیام نمیپذیرد."
+ actor_ignoring_target_user: "شما در حال نادیده گرفتن %{username} هستید، بنابراین نمیتوانید پیامی را برای او ارسال کنید."
+ actor_muting_target_user: "شما در حال بیصدا کردن %{username} هستید، بنابراین نمیتوانید پیامی را برای آنها ارسال کنید."
+ actor_disallowed_dms: "شما انتخاب کردهاید که از ارسال پیامهای خصوصی و پیامهای مستقیم در گفتگو توسط کاربران دیگر به شما جلوگیری کنیم، بنابراین نمیتوانید پیامهای مستقیم جدید در گفتگو ارسال کنید."
+ actor_preventing_target_user_from_dm: "شما انتخاب کردهاید که %{username}، از ارسال پیامهای خصوصی و پیامهای مستقیم در گفتگو برای شما جلوگیری کنیم، بنابراین نمیتوانید پیام مستقیم جدیدی در گفتگو برای او ارسال کنید."
+ user_cannot_send_direct_messages: "با عرض پوزش، شما نمیتوانید پیام مستقیم ارسال کنید."
+ reviewables:
+ message_already_handled: "با تشکر از شما، اما ما در حال حاضر این پیام را بررسی کردهایم و تشخیص دادهایم که نیازی به گزارش دوباره ندارد."
+ actions:
+ agree:
+ title: "موافقم..."
+ agree_and_suspend:
+ title: "کاربر تعلیق شده"
+ agree_and_delete:
+ title: "حذف پیام"
+ description: "پیام را حذف کنید تا کاربران نتوانند آن را ببینند."
+ delete_and_agree:
+ title: "حذف پیام"
+ disagree_and_restore:
+ title: "مخالفت و بازگرداندن پیام"
+ description: "پیام را بازیابی کنید تا همه کاربران بتوانند آن را ببینند."
+ disagree:
+ title: "مخالف"
+ ignore:
+ title: "چشم پوشی"
+ direct_messages:
+ transcript_title: "رونوشت پیامهای قبلی در %{channel_name}"
+ transcript_body: "برای ارائه متن بیشتر به شما، رونوشتی از پیامهای قبلی را در این گفتگو (حداکثر ده مورد) قرار دادیم:\n\n%{transcript}"
+ channel:
+ statuses:
+ read_only: "فقط خواندنی"
+ archived: "بایگانی شد"
+ closed: "بسته"
+ open: "باز"
+ dm_title:
+ single_user: "%{user}"
+ multi_user: "%{users}"
+ multi_user_truncated: "%{users} و %{leftover} نفر دیگر"
+ bookmarkable:
+ notification_title: "پیام در %{channel_name}"
+ personal_chat: "گفتگوی شخصی"
+ onebox:
+ inline_to_channel: "گفتگو #%{chat_channel}"
+ inline_to_topic_channel: "گفتگو برای موضوع %{topic_title}"
+ x_members:
+ one: "%{count} عضو"
+ other: "%{count} عضو"
+ and_x_others:
+ one: "و %{count} نفر دیگر"
+ other: "و %{count} نفر دیگر"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ title: ارسال پیام
+ reviewable_score_types:
+ needs_review:
+ title: "نیاز به بررسی دارد"
+ notify_user:
+ chat_pm_body: "%{link}\n\n%{message}"
+ notify_moderators:
+ chat_pm_body: "%{link}\n\n%{message}"
+ user_notifications:
+ chat_summary:
+ deleted_user: "کاربر حذف شده"
+ description:
+ one: "شما یک پیام گفتگو جدیدی دارید"
+ other: "شما پیامهای گفتگو جدیدی دارید"
+ from: "%{site_name}"
+ subject:
+ direct_message:
+ one: "[%{email_prefix}] پیام جدید از %{message_title}"
+ other: "[%{email_prefix}] پیام جدید %{message_title} و %{others}"
+ chat_channel:
+ one: "[%{email_prefix}] پیام جدید در %{message_title}"
+ other: "[%{email_prefix}] پیام جدید در %{message_title} و %{others}"
+ other_direct_message: "از %{message_title}"
+ others: "%{count} نفر دیگر"
+ view_messages:
+ one: "مشاهده پیام"
+ other: "مشاهده %{count} پیام"
+ view_more:
+ one: "مشاهده %{count} پیام بیشتر"
+ other: "مشاهده %{count} پیام بیشتر"
+ unsubscribe:
+ chat_summary:
+ never: هرگز
+ category:
+ cannot_delete:
+ has_chat_channels: "نمیتوان این دستهبندی را حذف کرد، چون دارای کانالهای گفتگو است"
diff --git a/plugins/chat/config/locales/server.fi.yml b/plugins/chat/config/locales/server.fi.yml
new file mode 100644
index 0000000000..1b557934e6
--- /dev/null
+++ b/plugins/chat/config/locales/server.fi.yml
@@ -0,0 +1,184 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+fi:
+ site_settings:
+ chat_allowed_groups: "Näiden ryhmien käyttäjät voivat keskustella chatissa. Huomaa, että henkilökunnalla on aina chatin käyttöoikeus."
+ chat_channel_retention_days: "Tavallisten kanavien chat-viestit säilytetään näin monta päivää. Viestit säilytetään ikuisesti, jos asetat arvoksi 0."
+ chat_dm_retention_days: "Henkilökohtaisten chat-kanavien chat-viestit säilytetään näin monta päivää. Viestit säilytetään ikuisesti, jos asetat arvoksi 0."
+ chat_auto_silence_duration: "Määrä minuutteina, jonka ajaksi käyttäjät hiljennetään heidän ylittäessään chat-viestien luontirajan. Voit poistaa automaattisen hiljentämisen käytöstä asettamalla arvoksi 0."
+ chat_allowed_messages_for_trust_level_0: "Viestien määrä, jonka luottamustason 0 käyttäjät voivat lähettää 30 sekunnin aikana. Voit poistaa rajan käytöstä asettamalla arvoksi 0."
+ chat_allowed_messages_for_other_trust_levels: "Viestien määrä, jonka luottamustasojen 1–4 käyttäjät voivat lähettää 30 sekunnin aikana. Voit poistaa rajan käytöstä asettamalla arvoksi 0."
+ chat_silence_user_sensitivity: "Todennäköisyys, että chatissa liputettu käyttäjä hiljennetään automaattisesti."
+ chat_auto_silence_from_flags_duration: "Määrä minuutteina, jonka ajaksi käyttäjät hiljennetään, kun heidät hiljennetään automaattisesti liputettujen chat-viestien takia."
+ chat_default_channel_id: "Chat-kanava, joka avataan oletuksena, kun käyttäjällä ei ole lukemattomia viestejä tai mainintoja muilla kanavilla."
+ chat_duplicate_message_sensitivity: "Todennäköisyys, että saman lähettäjän kaksoiskappaleviesti estetään lyhyen ajan sisällä. Desimaaliluku välillä 0–1,0, ja 1,0 on korkein asetus (estää viestit useammin lyhyemmässä ajassa). Voit sallia kaksoiskappaleviestit asettamalla arvoksi 0."
+ chat_minimum_message_length: "Chat-viestin merkkien vähimmäismäärä."
+ chat_allow_uploads: "Salli lataukset julkisilla chat-kanavilla ja yksityisviestikanavilla."
+ chat_archive_destination_topic_status: "Tila, jossa kohdeketjun tulisi olla, kun kanavan arkistointi on valmis. Tätä käytetään vain, kun kohdeketju on uusi ketju, ei olemassa oleva."
+ default_emoji_reactions: "Chat-viestien oletusarvoiset emoji-reaktiot. Lisää enintään 5 emojia nopeaa reagointia varten."
+ direct_message_enabled_groups: "Salli näiden ryhmien käyttäjien luoda käyttäjien välisiä henkilökohtaisia chat-keskusteluja. Huomautus: henkilökunta voi aina luoda henkilökohtaisia chat-keskusteluja, ja käyttäjät voivat vastata henkilökohtaisiin chat-keskusteluihin, jotka on aloittanut käyttäjä, jolla on oikeus luoda niitä."
+ chat_message_flag_allowed_groups: "Näiden ryhmien käyttäjät voivat liputtaa chat-viestejä."
+ errors:
+ chat_default_channel: "Oletus-chat-kanavan täytyy olla julkinen kanava."
+ direct_message_enabled_groups_invalid: "Sinun täytyy määrittää vähintään yksi ryhmä tälle asetukselle. Jos et halua muiden kuin henkilökunnan lähettävän yksityisviestejä, valitse henkilökunnan ryhmä."
+ chat_upload_not_allowed_secure_uploads: "Chat-lataukset eivät ole sallittuja, kun suojattujen latauksien sivustoasetus on käytössä."
+ system_messages:
+ chat_channel_archive_complete:
+ title: "Chat-kanavan arkistointi on valmis"
+ subject_template: "Chat-kanavan arkistointi on valmis"
+ text_body_template: |
+ Chat-kanavan **\#%{channel_name}** arkistointi on valmis. Viestit kopioitiin ketjuun [%{topic_title}](%{topic_url}).
+ chat_channel_archive_failed:
+ title: "Chat-kanavan arkistointi epäonnistui"
+ subject_template: "Chat-kanavan arkistointi epäonnistui"
+ text_body_template: |
+ Chat-kanavan **\#%{channel_name}** arkistointi epäonnistui. %{messages_archived} viestiä on arkistoitu. Osittain arkistoidut viestit kopioitiin ketjuun [%{topic_title}](%{topic_url}). Yritä uudelleen vierailemalla kanavalla osoitteessa %{channel_url}.
+ chat:
+ deleted_chat_username: poistettu
+ errors:
+ channel_exists_for_category: "Tällä alueella ja nimellä on jo olemassa kanava"
+ channel_new_message_disallowed: "Kanava on %{status}, uusia viestejä ei voi lähettää"
+ channel_modify_message_disallowed: "Kanava on %{status}, viestejä ei voi muokata tai poistaa"
+ user_cannot_send_message: "Et voi lähettää viestejä tällä hetkellä."
+ rate_limit_exceeded: "Ylitti 30 sekunnin sisällä lähetettävien chat-viestien rajan"
+ auto_silence_from_flags: "Chat-viesti liputettiin riittävän korkealla pistemäärällä käyttäjän hiljentämiseksi."
+ channel_cannot_be_archived: "Kanavaa ei voi arkistoida tällä hetkellä, sen täytyy olla suljettu tai avoinna, jotta sen voi arkistoida."
+ duplicate_message: "Lähetit identtisen viestin liian äskettäin."
+ delete_channel_failed: "Kanavan poistaminen epäonnistui, yritä uudelleen."
+ minimum_length_not_met: "Viesti on liian lyhyt, siinä täytyy olla vähintään %{minimum} merkkiä."
+ max_reactions_limit_reached: "Uusia reaktioita ei sallita tässä viestissä."
+ message_move_invalid_channel: "Lähde- ja kohdekanavan täytyy olla julkisia kanavia."
+ message_move_no_messages_found: "Annetuilla viestitunnuksilla ei löytynyt viestejä."
+ cant_update_direct_message_channel: "Yksityisviestikanavan ominaisuuksia, kuten nimeä ja kuvausta, ei voi päivittää."
+ not_accepting_dms: "%{username} ei ota vastaan viestejä tällä hetkellä."
+ actor_ignoring_target_user: "Ohitat käyttäjän %{username} tällä hetkellä, joten et voi lähettää hänelle viestejä."
+ actor_muting_target_user: "Mykistät käyttäjän %{username} tällä hetkellä, joten et voi lähettää hänelle viestejä."
+ actor_disallowed_dms: "Olet päättänyt estää käyttäjiä lähettämästä sinulle yksityisviestejä, joten et voi luoda uusia yksityisviestejä."
+ actor_preventing_target_user_from_dm: "Olet päättänyt estää käyttäjää %{username} lähettämästä sinulle yksityisviestejä, joten et voi luoda uusia yksityisviestejä hänelle."
+ user_cannot_send_direct_messages: "Valitettavasti et voi lähettää yksityisviestejä."
+ reviewables:
+ message_already_handled: "Kiitos, mutta olemme jo käsitelleet tämän viestin ja todenneet, ettei sitä tarvitse liputtaa uudelleen."
+ actions:
+ agree:
+ title: "Hyväksy..."
+ agree_and_keep_message:
+ title: "Säilytä viesti"
+ description: "Hyväksy liputus ja pidä viesti ennallaan."
+ agree_and_keep_deleted:
+ title: "Pidä viesti poistettuna"
+ description: "Hyväksy liputus ja pidä viesti poistettuna."
+ agree_and_suspend:
+ title: "Aseta käyttäjä käyttökieltoon"
+ description: "Hyväksy liputus ja aseta käyttäjä käyttökieltoon."
+ agree_and_silence:
+ title: "Hiljennä käyttäjä"
+ description: "Hyväksy liputus ja hiljennä käyttäjä."
+ agree_and_restore:
+ title: "Palauta viesti"
+ description: "Palauta viesti, jotta käyttäjät näkevät sen."
+ agree_and_delete:
+ title: "Poista viesti"
+ description: "Poista viesti, jotta käyttäjät eivät näe sitä."
+ delete_and_agree:
+ title: "Poista viesti"
+ disagree_and_restore:
+ title: "Hylkää ja palauta viesti"
+ description: "Palauta viesti, jotta kaikki käyttäjät näkevät sen."
+ disagree:
+ title: "Hylkää"
+ ignore:
+ title: "Ohita"
+ direct_messages:
+ transcript_title: "Kanavan %{channel_name} aiempien viestin transkriptio"
+ transcript_body: "Antaaksemme sinulle enemmän kontekstia lisäsimme tämän keskustelun aiempien viestien transkription (enintään kymmenen):\n\n%{transcript}"
+ channel:
+ statuses:
+ read_only: "Vain luku"
+ archived: "Arkistoitu"
+ closed: "Suljettu"
+ open: "Avoinna"
+ archive:
+ first_post_raw: "Tämä ketju on chat-kanavan [%{channel_name}](%{channel_url}) arkisto."
+ messages_moved:
+ one: "@%{acting_username} siirsi viestin kanavalle [%{channel_name}](%{first_moved_message_url})."
+ other: "@%{acting_username} siirsi %{count} viestiä kanavalle [%{channel_name}](%{first_moved_message_url})."
+ dm_title:
+ single_user: "%{user}"
+ multi_user: "%{users}"
+ multi_user_truncated: "%{users} ja %{leftover} muuta"
+ bookmarkable:
+ notification_title: "viesti kanavalla %{channel_name}"
+ personal_chat: "henkilökohtainen chat"
+ onebox:
+ inline_to_message: "Viesti %{message_id}, lähettäjä: %{username} – #%{chat_channel}"
+ inline_to_channel: "Chat #%{chat_channel}"
+ inline_to_topic_channel: "Ketjun %{topic_title} chat-keskustelu"
+ x_members:
+ one: "%{count} jäsen"
+ other: "%{count} jäsentä"
+ and_x_others:
+ one: "ja %{count} muu"
+ other: "ja %{count} muuta"
+ discourse_push_notifications:
+ popup:
+ chat_mention:
+ direct: '%{username} mainitsi sinut kanavalla "%{channel}"'
+ other_type: '%{username} mainitsi kohteen %{identifier} kanavalla "%{channel}"'
+ direct_message_chat_mention:
+ direct: "%{username} mainitsi sinut henkilökohtaisessa chatissa"
+ other_type: "%{username} mainitsi kohteen %{identifier} henkilökohtaisessa chatissa"
+ new_chat_message: '%{username} lähetti viestin kanavalla "%{channel}"'
+ new_direct_chat_message: "%{username} lähetti viestin henkilökohtaisessa chatissa"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ title: Lähetä chat-viesti
+ reviewable_score_types:
+ needs_review:
+ title: "Vaatii käsittelyä"
+ notify_user:
+ chat_pm_title: 'Chat-viestisi kanavalla "%{channel_name}"'
+ chat_pm_body: "%{link}\n\n%{message}"
+ notify_moderators:
+ chat_pm_title: 'Chat-viesti kanavalla "%{channel_name}" vaatii henkilökunnan huomiota'
+ chat_pm_body: "%{link}\n\n%{message}"
+ reviewables:
+ reasons:
+ chat_message_queued_by_staff: "Henkilökunnan jäsenen mielestä tämä chat-viesti täytyy tarkastaa."
+ user_notifications:
+ chat_summary:
+ deleted_user: "Poistettu käyttäjä"
+ description:
+ one: "Sinulla on uusi chat-viesti"
+ other: "Sinulla on uusia chat-viestejä"
+ from: "%{site_name}"
+ subject:
+ direct_message:
+ one: "[%{email_prefix}] Uusi viesti käyttäjältä %{message_title}"
+ other: "[%{email_prefix}] Uusia viestejä käyttäjältä %{message_title} ja %{others}"
+ chat_channel:
+ one: "[%{email_prefix}] Uusi viesti kanavalta %{message_title}"
+ other: "[%{email_prefix}] Uusia viestejä kanavalta %{message_title} ja %{others}"
+ other_direct_message: "käyttäjältä %{message_title}"
+ others: "ja %{count} muulta"
+ unsubscribe: "Tämä chat-yhteenveto lähetetään sivustolta %{site_link}, kun olet poissa. Määritä %{email_preferences_link} tai %{unsubscribe_link} peruuttaaksesi tilauksen."
+ unsubscribe_no_link: "Tämä chat-yhteenveto lähetetään sivustolta %{site_link}, kun olet poissa. Määritä %{email_preferences_link}."
+ view_messages:
+ one: "Näytä viesti"
+ other: "Näytä %{count} viestiä"
+ view_more:
+ one: "Näytä %{count} viesti lisää"
+ other: "Näytä %{count} viestiä lisää"
+ your_chat_settings: "chat-sähköpostien tiheysasetus"
+ unsubscribe:
+ chat_summary:
+ select_title: "Aseta chat-yhteenvetosähköpostien tiheydeksi:"
+ never: Ei koskaan
+ when_away: Vain poissa ollessa
+ category:
+ cannot_delete:
+ has_chat_channels: "Tätä aluetta ei voi poistaa, koska siinä on chat-kanavia."
diff --git a/plugins/chat/config/locales/server.fr.yml b/plugins/chat/config/locales/server.fr.yml
new file mode 100644
index 0000000000..0083fbd311
--- /dev/null
+++ b/plugins/chat/config/locales/server.fr.yml
@@ -0,0 +1,184 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+fr:
+ site_settings:
+ chat_allowed_groups: "Les utilisateurs de ces groupes peuvent discuter. Notez que les responsables peuvent toujours accéder à la discussion."
+ chat_channel_retention_days: "Les messages de discussion dans les canaux ordinaires seront conservés pendant ce nombre de jours. Fixez cette valeur sur « 0 » pour conserver les messages indéfiniment."
+ chat_dm_retention_days: "Les messages de discussion dans les discussions privées seront conservés pendant ce nombre de jours. Fixez cette valeur sur « 0 » pour conserver les messages indéfiniment."
+ chat_auto_silence_duration: "Nombre de minutes pendant lesquelles les utilisateurs seront mis en sourdine lorsqu'ils dépasseront la limite de création de messages de discussion. Réglez cette valeur sur « 0 » pour désactiver la mise en sourdine automatique."
+ chat_allowed_messages_for_trust_level_0: "Nombre de messages que les utilisateurs de niveau 0 sont autorisés à envoyer en 30 secondes. Réglez cette valeur sur « 0 » pour désactiver la limite."
+ chat_allowed_messages_for_other_trust_levels: "Nombre de messages que les utilisateurs avec des niveaux de confiance de 1 à 4 sont autorisés à envoyer en 30 secondes. Réglez cette valeur sur « 0 » pour désactiver la limite."
+ chat_silence_user_sensitivity: "La probabilité qu'un utilisateur signalé dans la discussion soit automatiquement mis en sourdine."
+ chat_auto_silence_from_flags_duration: "Nombre de minutes pendant lesquelles les utilisateurs sont mis en sourdine lorsqu'ils sont automatiquement mis en sourdine en raison de signalements de messages de discussion."
+ chat_default_channel_id: "Le canal de discussion qui est ouvert par défaut lorsqu'un utilisateur n'a aucun message ou mention non lue dans d'autres canaux."
+ chat_duplicate_message_sensitivity: "Probabilité qu'un message dupliqué du même expéditeur soit bloqué dans un court laps de temps. Nombre décimal compris entre 0 et 1,0, 1,0 étant le paramètre le plus élevé (bloque les messages plus fréquemment dans un laps de temps plus court). Réglez cette valeur sur « 0 » pour autoriser les messages dupliqués."
+ chat_minimum_message_length: "Nombre minimal de caractères pour un message de discussion."
+ chat_allow_uploads: "Autoriser les téléversements dans les canaux de discussion publics et les canaux de messagerie directe."
+ chat_archive_destination_topic_status: "Le statut que doit avoir le sujet de destination une fois l'archivage du canal terminé. Cela s'applique uniquement lorsque le sujet de destination est un nouveau sujet, et non un sujet existant."
+ default_emoji_reactions: "Réactions émoji par défaut pour les messages de discussion. Ajoutez jusqu'à 5 émojis pour une réaction rapide."
+ direct_message_enabled_groups: "Permettre aux utilisateurs de ces groupes de créer des discussions privées entre utilisateurs. Remarque : les responsables peuvent toujours créer des conversations privées et les utilisateurs pourront répondre aux conversations privées initiées par les utilisateurs qui sont autorisés à les créer."
+ chat_message_flag_allowed_groups: "Les utilisateurs de ces groupes sont autorisés à signaler les messages de discussion."
+ errors:
+ chat_default_channel: "Le canal de discussion par défaut doit être un canal public."
+ direct_message_enabled_groups_invalid: "Vous devez spécifier au moins un groupe pour ce paramètre. Si vous ne souhaitez pas que quiconque, à l'exception des responsables, envoie des messages privés, choisissez le groupe des responsables."
+ chat_upload_not_allowed_secure_uploads: "Les téléversements de discussion ne sont pas autorisés lorsque le paramètre du site de téléversement sécurisé est activé."
+ system_messages:
+ chat_channel_archive_complete:
+ title: "Archivage du canal de discussion terminé"
+ subject_template: "L'archivage du canal de discussion est terminé"
+ text_body_template: |
+ L'archivage du canal de discussion **\#%{channel_name}** a bien été effectué. Les messages ont été copiés dans le sujet [%{topic_title}](%{topic_url}).
+ chat_channel_archive_failed:
+ title: "Échec de l'archivage du canal de discussion"
+ subject_template: "Échec de l'archivage du canal de discussion"
+ text_body_template: |
+ L'archivage du canal de discussion **\#%{channel_name}** a échoué. Les messages de %{messages_archived} ont été archivés. Les messages partiellement archivés ont été copiés dans le sujet [%{topic_title}](%{topic_url}). Visitez le canal à l'adresse %{channel_url} pour réessayer.
+ chat:
+ deleted_chat_username: supprimé
+ errors:
+ channel_exists_for_category: "Un canal existe déjà pour cette catégorie et ce nom"
+ channel_new_message_disallowed: "Le canal a le statut %{status}, aucun nouveau message ne peut être envoyé"
+ channel_modify_message_disallowed: "Le canal a le statut %{status}, aucun message ne peut être modifié ou supprimé"
+ user_cannot_send_message: "Vous ne pouvez pas envoyer de messages pour le moment."
+ rate_limit_exceeded: "Dépassement de la limite de messages de discussion pouvant être envoyés dans les 30 secondes"
+ auto_silence_from_flags: "Message de discussion marqué avec un score suffisamment élevé pour mettre l'utilisateur en sourdine."
+ channel_cannot_be_archived: "Le canal ne peut pas être archivé pour le moment, il doit être soit fermé, soit ouvert à l'archivage."
+ duplicate_message: "Vous avez publié un message identique trop récemment."
+ delete_channel_failed: "Échec de la suppression du canal, veuillez réessayer."
+ minimum_length_not_met: "Le message est trop court. Il doit comporter au moins %{minimum} caractères."
+ max_reactions_limit_reached: "Les nouvelles réactions ne sont pas autorisées sur ce message."
+ message_move_invalid_channel: "Le canal source et le canal de destination doivent être des canaux publics."
+ message_move_no_messages_found: "Aucun message n'a été trouvé avec les ID de message fournis."
+ cant_update_direct_message_channel: "Les propriétés du canal de discussion privée telles que le nom et la description ne peuvent pas être mises à jour."
+ not_accepting_dms: "Nous sommes désolés, %{username} n'accepte pas les messages pour le moment."
+ actor_ignoring_target_user: "Vous ignorez %{username}, vous ne pouvez donc pas lui envoyer de messages."
+ actor_muting_target_user: "Vous mettez %{username} en sourdine, vous ne pouvez donc pas lui envoyer de messages."
+ actor_disallowed_dms: "Vous avez choisi d'empêcher les utilisateurs de vous envoyer des messages privés et directs, vous ne pouvez donc pas créer de nouveaux messages directs."
+ actor_preventing_target_user_from_dm: "Vous avez choisi d'empêcher %{username} de vous envoyer des messages privés et directs, vous ne pouvez donc pas lui envoyer de nouveaux messages privés."
+ user_cannot_send_direct_messages: "Nous sommes désolés, vous ne pouvez pas envoyer de messages privés."
+ reviewables:
+ message_already_handled: "Merci, mais nous avons déjà examiné ce message et déterminé qu'il n'a pas besoin d'être signalé à nouveau."
+ actions:
+ agree:
+ title: "D'accord…"
+ agree_and_keep_message:
+ title: "Conserver le message"
+ description: "Accepter le signalement et garder le message inchangé."
+ agree_and_keep_deleted:
+ title: "Garder le message supprimé"
+ description: "Accepter le signalement et laisser le message supprimé."
+ agree_and_suspend:
+ title: "Suspendre l'utilisateur"
+ description: "Accepter le signalement et suspendre l'utilisateur."
+ agree_and_silence:
+ title: "Désactiver l'utilisateur"
+ description: "Accepter le signalement et désactiver l'utilisateur."
+ agree_and_restore:
+ title: "Restaurer le message"
+ description: "Restaurer le message pour que les utilisateurs puissent le voir."
+ agree_and_delete:
+ title: "Supprimer le message"
+ description: "Supprimer le message pour que les utilisateurs ne puissent pas le voir."
+ delete_and_agree:
+ title: "Supprimer le message"
+ disagree_and_restore:
+ title: "Refuser et restaurer le message"
+ description: "Restaurer le message pour que tous les utilisateurs puissent le voir."
+ disagree:
+ title: "Refuser"
+ ignore:
+ title: "Ignorer"
+ direct_messages:
+ transcript_title: "Transcription des messages précédents dans le canal %{channel_name}"
+ transcript_body: "Pour vous donner plus de contexte, nous avons inclus une transcription des messages précédents de cette conversation (jusqu'à dix) :\n\n%{transcript}"
+ channel:
+ statuses:
+ read_only: "Lecture seule"
+ archived: "Archivé"
+ closed: "Fermé"
+ open: "Ouvert"
+ archive:
+ first_post_raw: "Ce sujet est une archive du canal de discussion [%{channel_name}](%{channel_url})."
+ messages_moved:
+ one: "@%{acting_username} a déplacé un message vers le canal [%{channel_name}](%{first_moved_message_url})."
+ other: "@%{acting_username} a déplacé %{count} messages vers le canal [%{channel_name}](%{first_moved_message_url})."
+ dm_title:
+ single_user: "%{user}"
+ multi_user: "%{users}"
+ multi_user_truncated: "%{users} et %{leftover} autres utilisateurs"
+ bookmarkable:
+ notification_title: "message dans %{channel_name}"
+ personal_chat: "discussion privée"
+ onebox:
+ inline_to_message: "Message #%{message_id} par %{username} - #%{chat_channel}"
+ inline_to_channel: "Discussion #%{chat_channel}"
+ inline_to_topic_channel: "Discussion pour le sujet %{topic_title}"
+ x_members:
+ one: "%{count} membre"
+ other: "%{count} membres"
+ and_x_others:
+ one: "et %{count} autre utilisateur"
+ other: "et %{count} autres utilisateurs"
+ discourse_push_notifications:
+ popup:
+ chat_mention:
+ direct: '%{username} vous a mentionné(e) dans « %{channel} »'
+ other_type: '%{username} a mentionné %{identifier} dans « %{channel} »'
+ direct_message_chat_mention:
+ direct: "%{username} vous a mentionné(e) dans la discussion privée"
+ other_type: "%{username} a mentionné %{identifier} dans la discussion privée"
+ new_chat_message: '%{username} a envoyé un message dans « %{channel} »'
+ new_direct_chat_message: "%{username} a envoyé un message dans la discussion privée"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ title: Envoyer un message de chat
+ reviewable_score_types:
+ needs_review:
+ title: "Nécessite un examen"
+ notify_user:
+ chat_pm_title: 'Votre message de discussion dans « %{channel_name} »'
+ chat_pm_body: "%{link}\n\n%{message}"
+ notify_moderators:
+ chat_pm_title: 'Un message de discussion dans « %{channel_name} » nécessite l''attention des responsables'
+ chat_pm_body: "%{link}\n\n%{message}"
+ reviewables:
+ reasons:
+ chat_message_queued_by_staff: "Un responsable pense que ce message de discussion doit être examiné."
+ user_notifications:
+ chat_summary:
+ deleted_user: "Utilisateur supprimé"
+ description:
+ one: "Vous avez un nouveau message de discussion"
+ other: "Vous avez de nouveaux messages de discussions"
+ from: "%{site_name}"
+ subject:
+ direct_message:
+ one: "[%{email_prefix}] Nouveau message de %{message_title}"
+ other: "[%{email_prefix}] Nouveaux messages de %{message_title} et de %{others}"
+ chat_channel:
+ one: "[%{email_prefix}] Nouveau message dans %{message_title}"
+ other: "[%{email_prefix}] Nouveaux messages dans %{message_title} et %{others}"
+ other_direct_message: "de %{message_title}"
+ others: "%{count} autres"
+ unsubscribe: "Ce résumé de discussion est envoyé à partir de %{site_link} lorsque vous vous absentez. Changez vos %{email_preferences_link} ou %{unsubscribe_link} pour vous désabonner."
+ unsubscribe_no_link: "Ce résumé de discussion est envoyé à partir de %{site_link} lorsque vous vous absentez. Changez vos %{email_preferences_link}."
+ view_messages:
+ one: "Voir le message"
+ other: "Voir %{count} messages"
+ view_more:
+ one: "Voir %{count} message supplémentaire"
+ other: "Voir %{count} messages supplémentaires"
+ your_chat_settings: "préférence de fréquence des e-mails de discussion"
+ unsubscribe:
+ chat_summary:
+ select_title: "Définissez la fréquence des e-mails de résumé de discussion sur :"
+ never: Jamais
+ when_away: Seulement en cas d'absence
+ category:
+ cannot_delete:
+ has_chat_channels: "Impossible de supprimer cette catégorie, car elle contient des canaux de discussion."
diff --git a/plugins/chat/config/locales/server.gl.yml b/plugins/chat/config/locales/server.gl.yml
new file mode 100644
index 0000000000..c1abe6a185
--- /dev/null
+++ b/plugins/chat/config/locales/server.gl.yml
@@ -0,0 +1,44 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+gl:
+ chat:
+ deleted_chat_username: eliminado
+ errors:
+ not_accepting_dms: "Sentímolo, %{username} non acepta mensaxes neste momento."
+ reviewables:
+ actions:
+ agree:
+ title: "De acordo..."
+ agree_and_suspend:
+ title: "Suspender un usuario"
+ description: "De acordo con esta denuncia e suspender o usuario."
+ agree_and_silence:
+ title: "Silenciar o usuario"
+ description: "De acordo con esta denuncia e silenciar o usuario."
+ disagree:
+ title: "Discrepar"
+ ignore:
+ title: "Ignorar"
+ channel:
+ statuses:
+ closed: "Pechado"
+ open: "Abrir"
+ discourse_push_notifications:
+ popup:
+ chat_mention:
+ direct: '%{username} mencionouno en "%{channel}"'
+ reviewable_score_types:
+ notify_user:
+ chat_pm_body: "%{link}\n\n%{message}"
+ notify_moderators:
+ chat_pm_body: "%{link}\n\n%{message}"
+ user_notifications:
+ chat_summary:
+ from: "%{site_name}"
+ unsubscribe:
+ chat_summary:
+ never: Nunca
diff --git a/plugins/chat/config/locales/server.he.yml b/plugins/chat/config/locales/server.he.yml
new file mode 100644
index 0000000000..81e3104a81
--- /dev/null
+++ b/plugins/chat/config/locales/server.he.yml
@@ -0,0 +1,201 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+he:
+ site_settings:
+ chat_enabled: "הפעלת תוסף הצ׳אט."
+ chat_allowed_groups: "משתמשים בקבוצות אלה יכולים לשוחח בצ׳אט. נא לשים לב שהסגל תמיד יכול לגשת לצ׳אט."
+ chat_channel_retention_days: "הודעות הצ׳אט בערוצים הרגילים ישמרו למשך כמות כזאת של ימים. הגדרה לאפס תשמור את ההודעות לעד."
+ chat_dm_retention_days: "הודעות הצ׳אט בערוצי הצ׳אט האישיים ישמרו למשך כמות כזאת של ימים. הגדרה לאפס תשמור את ההודעות לעד."
+ chat_auto_silence_duration: "מספר הדקות שבמהלכן שמשתמשים יושתקו כאשר הם חורגים ממגבלת קצב יצירת הודעת בצ׳אט. 0 משבית את ההשתקה האוטומטית."
+ chat_allowed_messages_for_trust_level_0: "מספר ההודעות שמשתמשים בדרגת אמון 0 רשאים לשלוח תוך 30 שניות. ‚0’ כדי להשבית את המגבלה."
+ chat_allowed_messages_for_other_trust_levels: "מספר ההודעות שמשתמשים בדרגות אמון 1-4 רשאים לשלוח תוך 30 שניות. ‚0’ כדי להשבית את המגבלה."
+ chat_silence_user_sensitivity: "הסבירות שמשתמש שסומן בצ׳אט יושתק אוטומטית."
+ chat_auto_silence_from_flags_duration: "מספר דקות השתקת המשתמשים כאשר הם מושתקים אוטומטית עקב הודעות צ׳אט מסומנות."
+ chat_default_channel_id: "ערוץ הצ׳אט שייפתח כברירת מחדל כאשר למשתמש אין הודעות או אזכורים שלא נקראו בערוצים אחרים."
+ chat_duplicate_message_sensitivity: "הסבירות שהודעה כפולה מאותו שולח תיחסם תוך זמן קצר. מספר עשרוני בין 0 ל־1.0, כאשר 1.0 הוא ההגדרה הגבוהה ביותר (חוסם הודעות בתדירות גבוהה יותר בפרק זמן קצר יותר). ‚0’ כדי לאפשר הודעות כפולות."
+ chat_minimum_message_length: "מספר התווים המזערי להודעת צ׳אט."
+ chat_allow_uploads: "לאפשר העלאות בערוצי צ׳אט ציבוריים ובערוצי הודעות ישירות."
+ chat_archive_destination_topic_status: "המצב בו נושא היעד צריך להיות לאחר שהעברת ערוץ לארכיון הושלמה. חל רק כאשר נושא היעד הוא נושא חדש ולא קיים."
+ default_emoji_reactions: "רגשות אמוג׳י כברירת מחדל להודעות צ׳אט. ניתן להוסיף עד 5 אמוג׳ים לתגובה מהירה."
+ direct_message_enabled_groups: "לאפשר למשתמשים בקבוצות אלה ליצור צ׳אטים אישיים בין המשתמשים לבין עצמם. הערה: הסגל תמיד יכול ליצור צ׳אטים אישיים, ומשתמשים יוכלו להשיב לצ׳אטים אישיים שיזמו משתמשים שיש להם הרשאה ליצור אותם."
+ chat_message_flag_allowed_groups: "משתמשים בקבוצות אלו רשאים לסמן הודעות צ׳אט בדגל."
+ errors:
+ chat_default_channel: "ערוץ הצ׳אט כברירת המחדל חייב להיות ערוץ ציבורי."
+ direct_message_enabled_groups_invalid: "יש לציין לפחות קבוצה אחת בהגדרה הזאת. כדי למנוע מכולם לשלוח הודעות ישירות למעט הסגל, יש לבחור בקבוצת הסגל."
+ chat_upload_not_allowed_secure_uploads: "אסור להעלות לצ׳אט כשהגדרת האתר להעלאות מאובטחות מופעלת."
+ system_messages:
+ chat_channel_archive_complete:
+ title: "העברת ערוץ הצ׳אט לארכיון הושלמה"
+ subject_template: "העברת ערוץ הצ׳אט לארכיון הושלמה בהצלחה"
+ text_body_template: |
+ העברת ערוץ הצ׳אט **\#%{channel_name}** לארכיון הושלמה בהצלחה. ההודעות הועתקו לנושא [%{topic_title}](%{topic_url}).
+ chat_channel_archive_failed:
+ title: "העברת הצ׳אט לארכיון נכשלה"
+ subject_template: "העברת הצ׳אט לארכיון נכשלה"
+ text_body_template: |
+ העברת ערוץ הצ׳אט **\#%{channel_name}** לארכיון נכשלה. %{messages_archived} הודעות הועברו לארכיון. הודעות שהועברו לארכיון באופן חלקי הועתקו לנושא [%{topic_title}](%{topic_url}). יש לבקר בכתובת הערוץ %{channel_url} כדי לנסות שוב.
+ chat:
+ deleted_chat_username: נמחק
+ errors:
+ channel_exists_for_category: "כבר קיים ערוץ לקטגוריה ולשם האלו"
+ channel_new_message_disallowed: "הערוץ %{status}, לא ניתן לשלוח הודעות חדשות"
+ channel_modify_message_disallowed: "הערוץ %{status}, לא ניתן לערוך או למחוק הודעות"
+ user_cannot_send_message: "אין לך אפשרות לשלוח הודעות כרגע."
+ rate_limit_exceeded: "חריגה ממגבלת הודעות הצ׳אט שניתן לשלוח תוך 30 שניות"
+ auto_silence_from_flags: "הודעת צ׳אט שסומנה בציון גבוה מספיק כדי להשתיק את המשתמש."
+ channel_cannot_be_archived: "אי אפשר להעביר את הערוץ לארכיון כרגע, הוא חייב להיות סגור או פתוח להעברה לארכיון."
+ duplicate_message: "פרסמת הודעה זהה לפני זמן קצר מדי."
+ delete_channel_failed: "מחיקת הערוץ נכשלה, נא לנסות שוב."
+ minimum_length_not_met: "ההודעה קצרה מדי, היא חייבת להיות ארוכה מ־%{minimum} תווים"
+ max_reactions_limit_reached: "רגשות חדשים אסורים בהודעה זו."
+ message_move_invalid_channel: "ערוצי המקור והיעד חייבים להיות ערוצים ציבוריים."
+ message_move_no_messages_found: "לא נמצאו הודעות עם מזהי ההודעות שסופקו."
+ cant_update_direct_message_channel: "מאפייני ערוץ הודעות ישירות כמו שם ותיאור נעולים מפני עדכון."
+ not_accepting_dms: "מצטעים, %{username} לא מקבל הודעות כרגע."
+ actor_ignoring_target_user: "בחרת להתעלם מ־%{username}, כך שאין לך אפשרות לשלוח אליהם הודעות."
+ actor_muting_target_user: "בחרת להשתיק את %{username}, כך שאין לך אפשרות לשלוח אליהם הודעות."
+ actor_disallowed_dms: "בחרת למנוע ממשתמשים לשלוח אליך הודעות פרטיות וישירות כך שאין לך אפשרות ליצור הודעות ישירות חדשות."
+ actor_preventing_target_user_from_dm: "בחרת למנוע מ־%{username} לשלוח אליך הודעות פרטיות וישירות כך שאין לך אפשרות ליצור הודעות ישירות חדשות אליהם."
+ user_cannot_send_direct_messages: "מחילה, אין לך אפשרות לשלוח הודעות ישירות."
+ reviewables:
+ message_already_handled: "תודה, אבל כבר סקרנו הודעה זו וקבענו שאין צורך לסמן אותה שוב."
+ actions:
+ agree:
+ title: "הסכמה…"
+ agree_and_keep_message:
+ title: "להשאיר את ההודעה"
+ description: "להסכים עם הסימון ולהשאיר את ההודעה ללא שינוי."
+ agree_and_keep_deleted:
+ title: "להשאיר את ההודעה מחוקה"
+ description: "להסכים עם הסימון ולהשאיר את ההודעה מחוקה."
+ agree_and_suspend:
+ title: "השעיית משתמש"
+ description: "הבעת הסכמה עם הדגל והשעיית המשתמש."
+ agree_and_silence:
+ title: "השתקת משתמש"
+ description: "הבעת הסכמה עם הדגל והשתקת המשתמש."
+ agree_and_restore:
+ title: "שחזור הודעה"
+ description: "לשחזר את ההודעה כדי שמשתמשים יוכלו לראות אותה."
+ agree_and_delete:
+ title: "מחיקת ההודעה"
+ description: "למחוק את ההודעה כדי שמשתמשים לא יוכלו לראות אותה."
+ delete_and_agree:
+ title: "מחיקת ההודעה"
+ disagree_and_restore:
+ title: "חוסר הסכמה ושחזור ההודעה"
+ description: "לשחזר את ההודעה כדי שכל המשתמשים יוכלו לראות אותה."
+ disagree:
+ title: "אי קבלה"
+ ignore:
+ title: "התעלמות"
+ direct_messages:
+ transcript_title: "תמלול הודעות קודמות בערוץ %{channel_name}"
+ transcript_body: "כדי לתת לך יותר הקשר, הוספנו תמליל של (עד עשר) ההודעות הקודמות בשיחה זו:\n\n%{transcript}"
+ channel:
+ statuses:
+ read_only: "לקריאה בלבד"
+ archived: "בארכיון"
+ closed: "סגורה"
+ open: "פתיחה"
+ archive:
+ first_post_raw: "הנושא הזה הוא הארכיון של ערוץ הצ׳אט [%{channel_name}](%{channel_url})."
+ messages_moved:
+ one: "הודעה הועברה על ידי @%{acting_username} לערוץ [%{channel_name}](%{first_moved_message_url})."
+ two: "%{count} הודעות הועברו על ידי @%{acting_username} לערוץ [%{channel_name}](%{first_moved_message_url})."
+ many: "%{count} הודעות הועברו על ידי @%{acting_username} לערוץ [%{channel_name}](%{first_moved_message_url})."
+ other: "%{count} הודעות הועברו על ידי @%{acting_username} לערוץ [%{channel_name}](%{first_moved_message_url})."
+ dm_title:
+ single_user: "%{user}"
+ multi_user: "%{users}"
+ multi_user_truncated: "%{users} ועוד %{leftover}"
+ bookmarkable:
+ notification_title: "הודעה ב־%{channel_name}"
+ personal_chat: "צ׳אט אישי"
+ onebox:
+ inline_to_message: "הודעה מס׳ %{message_id} מאת %{username} – #%{chat_channel}"
+ inline_to_channel: "צ׳אט מס׳ %{chat_channel}"
+ inline_to_topic_channel: "צ׳אט לנושא %{topic_title}"
+ x_members:
+ one: "חבר %{count}"
+ two: "%{count} חברים"
+ many: "%{count} חברים"
+ other: "%{count} חברים"
+ and_x_others:
+ one: "ועוד %{count}"
+ two: "ו־%{count} נוספים"
+ many: "ו־%{count} נוספים"
+ other: "ו־%{count} נוספים"
+ discourse_push_notifications:
+ popup:
+ chat_mention:
+ direct: 'אוזכרת בערוץ „%{channel}” על ידי %{username}'
+ other_type: 'נוסף אזכור של %{identifier} בערוץ „%{channel}” על ידי %{username}'
+ direct_message_chat_mention:
+ direct: "אוזכרת בצ׳אט אישי על ידי %{username}"
+ other_type: "נוסף אזכור של %{identifier} בצ׳אט אישי על ידי %{username}"
+ new_chat_message: 'נשלחה הודעה על ידי %{username} ב־„%{channel}”'
+ new_direct_chat_message: "נשלחה הודעה על ידי %{username} בצ׳אט אישי"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ title: שליחת הודעת צ׳אט
+ reviewable_score_types:
+ needs_review:
+ title: "נדרשת סקירה"
+ notify_user:
+ chat_pm_title: 'הודעת הצ׳אט שלך תחת „%{channel_name}”'
+ chat_pm_body: "%{link}\n\n%{message}"
+ notify_moderators:
+ chat_pm_title: 'הודעת צ׳אט בערוץ „%{channel_name}” דורשת את תשומת לב הסגל'
+ chat_pm_body: "%{link}\n\n%{message}"
+ reviewables:
+ reasons:
+ chat_message_queued_by_staff: "חבר סגל חושב שהודעת צ׳אט זו דורשת בדיקה."
+ user_notifications:
+ chat_summary:
+ deleted_user: "משתמש שנמחק"
+ description:
+ one: "יש לך הודעה חדשה בצ׳אט"
+ two: "יש לך הודעות חדשות בצ׳אט"
+ many: "יש לך הודעות חדשות בצ׳אט"
+ other: "יש לך הודעות חדשות בצ׳אט"
+ from: "%{site_name}"
+ subject:
+ direct_message:
+ one: "[%{email_prefix}] הודעה חדשה מאת %{message_title}"
+ two: "[%{email_prefix}] הודעה חדשה מאת %{message_title} ועוד %{others}"
+ many: "[%{email_prefix}] הודעה חדשה מאת %{message_title} ועוד %{others}"
+ other: "[%{email_prefix}] הודעה חדשה מאת %{message_title} ועוד %{others}"
+ chat_channel:
+ one: "[%{email_prefix}] הודעה חדשה תחת %{message_title}"
+ two: "[%{email_prefix}] הודעה חדשה תחת %{message_title} ועוד %{others}"
+ many: "[%{email_prefix}] הודעה חדשה תחת %{message_title} ועוד %{others}"
+ other: "[%{email_prefix}] הודעה חדשה תחת %{message_title} ועוד %{others}"
+ other_direct_message: "מאת %{message_title}"
+ others: "%{count} נוספים"
+ unsubscribe: "סיכום צ׳אט זה נשלח מהאתר %{site_link} כשנעדרת ממנו. ניתן לשנות את %{email_preferences_link} שלך או %{unsubscribe_link} כדי להפסיק לקבל הודעות."
+ unsubscribe_no_link: "סיכום צ׳אט זה נשלח מהאתר %{site_link} כשנעדרת ממנו. ניתן לשנות את %{email_preferences_link} שלך."
+ view_messages:
+ one: "הצגת הודעה"
+ two: "הצגת %{count} הודעות"
+ many: "הצגת %{count} הודעות"
+ other: "הצגת %{count} הודעות"
+ view_more:
+ one: "הצגת הודעה נוספת %{count}"
+ two: "הצגת %{count} הודעות נוספות"
+ many: "הצגת %{count} הודעות נוספות"
+ other: "הצגת %{count} הודעות נוספות"
+ your_chat_settings: "העדפת תדירות דוא״ל צ׳אט"
+ unsubscribe:
+ chat_summary:
+ select_title: "הגדרת תדירות הודעות סיכום בדוא״ל ל־:"
+ never: לעולם לא
+ when_away: רק כשלא במערכת
+ category:
+ cannot_delete:
+ has_chat_channels: "לא ניתן למחוק את הקטגוריה הזו כי יש לה ערוצי צ׳אט."
diff --git a/plugins/chat/config/locales/server.hr.yml b/plugins/chat/config/locales/server.hr.yml
new file mode 100644
index 0000000000..2f600fb0c0
--- /dev/null
+++ b/plugins/chat/config/locales/server.hr.yml
@@ -0,0 +1,37 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+hr:
+ chat:
+ deleted_chat_username: izbrisao
+ errors:
+ not_accepting_dms: "Žao nam je, %{username} trenutno ne prihvaća poruke."
+ reviewables:
+ actions:
+ agree_and_suspend:
+ title: "Suspendiraj korisnika"
+ agree_and_silence:
+ title: "Ušuti korisnika"
+ disagree:
+ title: "Odbaci"
+ ignore:
+ title: "Zanemari"
+ channel:
+ statuses:
+ closed: "Zatvoreno"
+ open: "Otvori"
+ discourse_push_notifications:
+ popup:
+ chat_mention:
+ direct: '%{username} vas je spomenuo u "%{channel}"'
+ reviewable_score_types:
+ notify_user:
+ chat_pm_body: "%{link}\n\n%{message}"
+ notify_moderators:
+ chat_pm_body: "%{link}\n\n%{message}"
+ unsubscribe:
+ chat_summary:
+ never: Nikad
diff --git a/plugins/chat/config/locales/server.hu.yml b/plugins/chat/config/locales/server.hu.yml
new file mode 100644
index 0000000000..f8d08804ec
--- /dev/null
+++ b/plugins/chat/config/locales/server.hu.yml
@@ -0,0 +1,151 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+hu:
+ site_settings:
+ chat_channel_retention_days: "A normál csatornákon lévő csevegőüzenetek ennyi napig maradnak meg. Állítsa „0”-ra, hogy örökre megtartsa az üzeneteket."
+ chat_dm_retention_days: "A személyes csevegési csatornákon lévő csevegőüzenetek ennyi napig maradnak meg. Állítsa „0”-ra, hogy örökre megtartsa az üzeneteket."
+ chat_auto_silence_duration: "A felhasználók ennyi percig lesznek némítva, ha túllépik a csevegőüzenet létrehozási korlátját. Állítsa „0”-ra, hogy letiltsa az automatikus némítást."
+ chat_allowed_messages_for_trust_level_0: "A 0-s megbízhatósági szinttel rendelkező felhasználók legfeljebb ennyi üzenetet küldhetnek 30 másodpercen belül. A korlátozás letiltásához állítsa „0”-ra."
+ chat_allowed_messages_for_other_trust_levels: "Az 1–4-es megbízhatósági szinttel rendelkező felhasználók legfeljebb ennyi üzenetet küldhetnek 30 másodpercen belül. A korlátozás letiltásához állítsa „0”-ra."
+ chat_silence_user_sensitivity: "Annak a valószínűsége, hogy a csevegésben megjelölt felhasználó automatikusan némítva lesz."
+ chat_auto_silence_from_flags_duration: "Azon percek száma, ameddig a felhasználók el lesznek némítva, ha a megjelölt csevegési üzenetek miatt automatikusan némítva lesznek."
+ chat_default_channel_id: "Az a csevegőcsatorna, amely alapértelmezés szerint megnyílik, ha a felhasználónak nincsenek olvasatlan üzenetei vagy említései más csatornákon."
+ chat_duplicate_message_sensitivity: "Annak a valószínűsége, hogy az azonos feladó által küldött ismételt üzenet rövid időn belül blokkolásra kerül. Tizedes szám 0 és 1.0 között, ahol az 1.0 a legmagasabb érték (az üzeneteket gyakrabban blokkolja rövidebb idő alatt). Az ismételt üzenetek engedélyezéséhez állítsa „0”-ra az értéket."
+ chat_minimum_message_length: "A csevegőüzenetek minimális karakterszáma."
+ chat_archive_destination_topic_status: "Az az állapot, amelyet a céltéma a csatornaarchiválás befejezése után fog kapni. Ez csak akkor érvényes, ha a céltéma egy új téma, nem pedig egy meglévő."
+ errors:
+ chat_default_channel: "Az alapértelmezett csevegőcsatornának nyilvános csatornának kell lennie."
+ system_messages:
+ chat_channel_archive_complete:
+ title: "A csevegőcsatorna archiválása kész"
+ subject_template: "A csevegőcsatorna archiválása sikeresen befejeződött"
+ text_body_template: |
+ A(z) **\#%{channel_name}** csevegőcsatorna archiválása sikeresen befejeződött. Az üzenetek átmásolásra kerültek a(z) [ <%{topic_title}](%{topic_url}) témába.
+ chat_channel_archive_failed:
+ title: "A csevegőcsatorna archiválása sikertelen"
+ subject_template: "A csevegőcsatorna archiválása sikertelen"
+ text_body_template: |
+ A(z) **\#%{channel_name}** csevegőcsatorna archiválása nem sikerült. %{messages_archived} üzenet archiválásra került. A részben archivált üzeneteket a(z) [%{topic_title}](%{topic_url}) témába lettek másolva. Keresse fel a(z) %{channel_url} csatornát az újbóli próbálkozáshoz.
+ chat:
+ deleted_chat_username: törölt
+ errors:
+ channel_exists_for_category: "Már létezik csatorna ehhez a kategóriával, és ezzel a névvel"
+ channel_new_message_disallowed: "A csatorna „%{status}”, új üzenet nem küldhető"
+ channel_modify_message_disallowed: "A csatorna „%{status}”, az üzenetek nem szerkeszthetők vagy törölhetők"
+ user_cannot_send_message: "Jelenleg nem küldhet üzeneteket."
+ rate_limit_exceeded: "Túllépte a 30 másodpercen belül elküldhető csevegőüzenetek korlátját"
+ auto_silence_from_flags: "A csevegőüzenet elég magas pontszámmal lett megjelölve, hogy a felhasználó némítva legyen."
+ channel_cannot_be_archived: "A csatorna jelenleg nem archiválható, a csatornát vagy le kell zárni, vagy meg kell nyitni az archiváláshoz."
+ duplicate_message: "Nemrég küldött egy azonos tartalmú üzenetet."
+ delete_channel_failed: "A csatorna törlése sikertelen, próbálja meg újra."
+ minimum_length_not_met: "Az üzenet túl rövid, legalább %{minimum} karaktert kell tartalmaznia."
+ max_reactions_limit_reached: "Új reakciók nem engedélyezettek ezen az üzeneten."
+ message_move_invalid_channel: "A forrás- és célcsatornának nyilvános csatornának kell lennie."
+ message_move_no_messages_found: "A megadott üzenetazonosítókkal nem találhatók üzenetek."
+ reviewables:
+ actions:
+ agree:
+ title: "Egyetértek…"
+ agree_and_keep_message:
+ title: "Üzenet megtartása"
+ description: "Egyetért a jelentéssel, és változatlanul hagyja az üzenetet."
+ agree_and_keep_deleted:
+ title: "Üzenet törölve hagyása"
+ description: "Egyetért a jelentéssel, és törölve hagyja az üzenetet."
+ agree_and_suspend:
+ title: "Felhasználó felfüggesztése"
+ description: "Egyetértés a megjelöléssel, és a felhasználó felfüggesztése"
+ agree_and_silence:
+ title: "Felhasználó némítása"
+ description: "Egyetértés a megjelöléssel, és a felhasználó némítása"
+ agree_and_restore:
+ title: "Üzenet helyreállítása"
+ description: "Üzenet helyreállítása, hogy láthassák a felhasználók."
+ agree_and_delete:
+ title: "Üzenet törlése"
+ description: "Üzenet törlése, hogy a felhasználók ne láthassák."
+ delete_and_agree:
+ title: "Üzenet törlése"
+ disagree_and_restore:
+ title: "Nem ért egyet, és az üzenet helyreállítása"
+ description: "Üzenet helyreállítása, hogy az összes felhasználó láthassa."
+ disagree:
+ title: "Elutasítás"
+ ignore:
+ title: "Letiltás"
+ channel:
+ statuses:
+ read_only: "Csak olvasható"
+ archived: "Archivált"
+ closed: "Zárt"
+ open: "Megnyitás"
+ archive:
+ first_post_raw: "Ez a téma a(z) [%{channel_name}](%{channel_url}) csevegőcsatorna archívuma."
+ messages_moved:
+ one: "@%{acting_username} áthelyezett egy üzenetet a(z) [%{channel_name}](%{first_moved_message_url}) csatornába."
+ other: "@%{acting_username} áthelyezett %{count} üzenetet a(z) [%{channel_name}](%{first_moved_message_url}) csatornába."
+ dm_title:
+ single_user: "%{user}"
+ multi_user: "%{users}"
+ multi_user_truncated: "%{users} és még %{leftover} fő"
+ personal_chat: "személyes csevegés"
+ onebox:
+ x_members:
+ one: "%{count} tag"
+ other: "%{count} tag"
+ and_x_others:
+ one: "és még %{count} fő"
+ other: "és még %{count} fő"
+ discourse_push_notifications:
+ popup:
+ chat_mention:
+ direct: '%{username} megemlítette Önt a következő csatornán: „%{channel}”'
+ other_type: '%{username} megemlítette %{identifier} felhasználót a következő csatornán: „%{channel}”'
+ direct_message_chat_mention:
+ direct: "%{username} megemlítette Önt egy személyes csevegésben"
+ other_type: "%{username} megemlítette %{identifier} felhasználót egy személyes csevegésben"
+ new_chat_message: '%{username} üzenet küldött a következő csatornán: „%{channel}”'
+ new_direct_chat_message: "%{username} üzenetet küldött egy személyes csevegésben"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ title: Csevegőüzenet küldése
+ reviewable_score_types:
+ needs_review:
+ title: "Felülvizsgálatra szorul"
+ notify_user:
+ chat_pm_body: "%{link}\n\n%{message}"
+ notify_moderators:
+ chat_pm_body: "%{link}\n\n%{message}"
+ user_notifications:
+ chat_summary:
+ deleted_user: "Törölt felhasználó"
+ description:
+ one: "Új csevegőüzenete érkezett"
+ other: "Új csevegőüzenetei érkeztek"
+ from: "%{site_name}"
+ subject:
+ direct_message:
+ one: "[%{email_prefix}] Új üzenet a következőtől: %{message_title}"
+ other: "[%{email_prefix}] Új üzenetek a következőktől: %{message_title} és %{others}"
+ other_direct_message: "a következőtől: %{message_title}"
+ others: "és még %{count} fő"
+ view_messages:
+ one: "Üzenet megtekintése"
+ other: "%{count} üzenet megtekintése"
+ view_more:
+ one: "%{count} további üzenet megtekintése"
+ other: "%{count} további üzenet megtekintése"
+ your_chat_settings: "csevegési e-mail gyakoriságának beállítása"
+ unsubscribe:
+ chat_summary:
+ select_title: "A csevegési összefoglaló e-mailek gyakoriságának beállítása:"
+ never: Soha
+ when_away: Csak ha távol van
+ category:
+ cannot_delete:
+ has_chat_channels: "Ezt a kategóriát nem lehet törölni, mert csevegőcsatornái vannak."
diff --git a/plugins/chat/config/locales/server.hy.yml b/plugins/chat/config/locales/server.hy.yml
new file mode 100644
index 0000000000..6248aff03a
--- /dev/null
+++ b/plugins/chat/config/locales/server.hy.yml
@@ -0,0 +1,39 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+hy:
+ chat:
+ deleted_chat_username: ջնջված
+ errors:
+ not_accepting_dms: "Ներողություն, %{username}-ը այս պահին հաղորդագրություններ չի ընդունում:"
+ reviewables:
+ actions:
+ agree_and_suspend:
+ title: "Սառեցնել Օգտատիրոջը"
+ description: "Ընդունել դրոշակավորումը և սառեցնել օգտատիրոջը:"
+ agree_and_silence:
+ title: "Լռեցնել Օգտատիրոջը"
+ description: "Ընդունել դրոշակը և սառեցնել օգտատիրոջը:"
+ disagree:
+ title: "Չընդունել"
+ ignore:
+ title: "Անտեսել"
+ channel:
+ statuses:
+ closed: "Փակված"
+ open: "Բացել"
+ discourse_push_notifications:
+ popup:
+ chat_mention:
+ direct: '%{username} -ը նշել է Ձեզ այստեղ՝ "%{channel}"'
+ reviewable_score_types:
+ notify_user:
+ chat_pm_body: "%{link}\n\n%{message}"
+ notify_moderators:
+ chat_pm_body: "%{link}\n\n%{message}"
+ unsubscribe:
+ chat_summary:
+ never: Երբեք
diff --git a/plugins/chat/config/locales/server.id.yml b/plugins/chat/config/locales/server.id.yml
new file mode 100644
index 0000000000..7b8b81cc5c
--- /dev/null
+++ b/plugins/chat/config/locales/server.id.yml
@@ -0,0 +1,25 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+id:
+ chat:
+ deleted_chat_username: dihapus
+ reviewables:
+ actions:
+ ignore:
+ title: "Abaikan"
+ channel:
+ statuses:
+ closed: "Tertutup"
+ open: "Buka"
+ reviewable_score_types:
+ notify_user:
+ chat_pm_body: "%{link}\n\n%{message}"
+ notify_moderators:
+ chat_pm_body: "%{link}\n\n%{message}"
+ unsubscribe:
+ chat_summary:
+ never: Tidak pernah
diff --git a/plugins/chat/config/locales/server.it.yml b/plugins/chat/config/locales/server.it.yml
new file mode 100644
index 0000000000..6b935f8dd1
--- /dev/null
+++ b/plugins/chat/config/locales/server.it.yml
@@ -0,0 +1,184 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+it:
+ site_settings:
+ chat_allowed_groups: "Gli utenti di questi gruppi possono chattare. Tieni presente che lo staff può sempre accedere alla chat."
+ chat_channel_retention_days: "I messaggi di chat nei canali normali saranno conservati per i giorni indicati. Impostare il parametro a '0' per conservare i messaggi per sempre."
+ chat_dm_retention_days: "I messaggi di chat nei canali di chat personali saranno conservati per i giorni indicati. Impostare il parametro a '0' per conservare i messaggi per sempre."
+ chat_auto_silence_duration: "Numero di minuti per i quali gli utenti verranno silenziati quando superano il limite di velocità di creazione dei messaggi di chat. Imposta il valore su '0' per disabilitare il silenziamento automatico."
+ chat_allowed_messages_for_trust_level_0: "Numero di messaggi che gli utenti con livello di attendibilità 0 possono inviare in 30 secondi. Imposta su '0' per disabilitare il limite."
+ chat_allowed_messages_for_other_trust_levels: "Numero di messaggi che gli utenti con livello di attendibilità 1-4 possono inviare in 30 secondi. Imposta su '0' per disabilitare il limite."
+ chat_silence_user_sensitivity: "La probabilità che un utente segnalato in chat venga automaticamente silenziato."
+ chat_auto_silence_from_flags_duration: "Numero di minuti per i quali gli utenti verranno silenziati automaticamente a causa di messaggi di chat segnalati."
+ chat_default_channel_id: "Il canale di chat che verrà aperto per impostazione predefinita quando un utente non ha messaggi non letti o menzioni in altri canali."
+ chat_duplicate_message_sensitivity: "La probabilità che un messaggio duplicato dello stesso mittente venga bloccato in breve tempo. Numero decimale compreso tra 0 e 1,0, dove 1,0 è l'impostazione più alta (blocca i messaggi più frequentemente in un lasso di tempo più breve). Imposta su `0` per consentire messaggi duplicati."
+ chat_minimum_message_length: "Numero minimo di caratteri per un messaggio di chat."
+ chat_allow_uploads: "Consenti caricamenti nei canali di chat pubblici e nei canali di messaggistica diretta."
+ chat_archive_destination_topic_status: "Lo stato che dovrebbe avere l'argomento di destinazione una volta completata l'archiviazione di un canale. Questa opzione si applica solo quando l'argomento di destinazione è nuovo, non già esistente."
+ default_emoji_reactions: "Reazioni emoji predefinite per i messaggi di chat. Aggiungi fino a 5 emoji per una reazione rapida."
+ direct_message_enabled_groups: "Consenti agli utenti all'interno di questi gruppi di creare chat personali da utente a utente. Nota: lo staff può sempre creare chat personali e gli utenti potranno rispondere alle chat personali avviate da utenti che dispongono dell'autorizzazione per crearle."
+ chat_message_flag_allowed_groups: "Gli utenti di questi gruppi possono contrassegnare i messaggi di chat."
+ errors:
+ chat_default_channel: "Il canale di chat predefinito deve essere un canale pubblico."
+ direct_message_enabled_groups_invalid: "Devi specificare almeno un gruppo per questa impostazione. Se non vuoi che nessuno al di fuori dello staff possa inviare messaggi diretti, scegli il gruppo dello staff."
+ chat_upload_not_allowed_secure_uploads: "I caricamenti di chat non sono consentiti quando l'impostazione del sito per i caricamenti sicuri è abilitata."
+ system_messages:
+ chat_channel_archive_complete:
+ title: "Archiviazione canale chat completata"
+ subject_template: "Archiviazione del canale di chat completata correttamente"
+ text_body_template: |
+ L'archiviazione del canale di chat **\#%{channel_name}** è stata completata con successo. I messaggi sono stati copiati nell'argomento [%{topic_title}](%{topic_url}).
+ chat_channel_archive_failed:
+ title: "Archiviazione canale chat non riuscita"
+ subject_template: "Archiviazione canale chat non riuscita"
+ text_body_template: |
+ L'archiviazione del canale di chat **\#%{channel_name}** non è riuscita. %{messages_archived} messaggi sono stati archiviati. I messaggi parzialmente archiviati sono stati copiati nell'argomento [%{topic_title}](%{topic_url}). Visita il canale in %{channel_url} per riprovare.
+ chat:
+ deleted_chat_username: eliminato
+ errors:
+ channel_exists_for_category: "Esiste già un canale per questa categoria e questo nome"
+ channel_new_message_disallowed: "Il canale è %{status}, non è possibile inviare nuovi messaggi"
+ channel_modify_message_disallowed: "Il canale è %{status}, nessun messaggio può essere modificato o cancellato"
+ user_cannot_send_message: "In questo momento non puoi inviare messaggi."
+ rate_limit_exceeded: "Superato il limite dei messaggi di chat che possono essere inviati in 30 secondi"
+ auto_silence_from_flags: "Messaggio di chat contrassegnato con un punteggio sufficientemente alto per silenziare l'utente."
+ channel_cannot_be_archived: "Il canale non può essere archiviato in questo momento, deve essere chiuso o aperto per l'archiviazione."
+ duplicate_message: "Hai pubblicato un messaggio identico troppo di recente."
+ delete_channel_failed: "Eliminazione del canale non riuscita, riprova."
+ minimum_length_not_met: "Il messaggio è troppo breve, deve contenere almeno %{minimum} caratteri."
+ max_reactions_limit_reached: "Non sono consentite nuove reazioni su questo messaggio."
+ message_move_invalid_channel: "I canali di origine e di destinazione devono essere canali pubblici."
+ message_move_no_messages_found: "Nessun messaggio è stato trovato con gli ID messaggio forniti."
+ cant_update_direct_message_channel: "Le proprietà del canale dei messaggi diretti come il nome e la descrizione non possono essere aggiornate."
+ not_accepting_dms: "Spiacenti, %{username} non accetta messaggi al momento."
+ actor_ignoring_target_user: "Stai ignorando %{username}, quindi non puoi inviare messaggi a questo destinatario."
+ actor_muting_target_user: "Hai silenziato %{username}, quindi non puoi inviare messaggi a questo destinatario."
+ actor_disallowed_dms: "Hai scelto di impedire agli utenti di inviarti messaggi privati e diretti, quindi non puoi creare nuovi messaggi diretti."
+ actor_preventing_target_user_from_dm: "Hai scelto di impedire a %{username} di inviarti messaggi privati e diretti, quindi non puoi creare nuovi messaggi diretti per questo destinatario."
+ user_cannot_send_direct_messages: "Spiacenti, non puoi inviare messaggi diretti."
+ reviewables:
+ message_already_handled: "Grazie, ma abbiamo già esaminato questo messaggio e stabilito che non è necessario contrassegnarlo di nuovo."
+ actions:
+ agree:
+ title: "Accetta..."
+ agree_and_keep_message:
+ title: "Conserva messaggio"
+ description: "Accetta segnalazione e mantieni il messaggio invariato."
+ agree_and_keep_deleted:
+ title: "Conferma eliminazione del messaggio"
+ description: "Accetta segnalazione e conferma eliminazione del messaggio."
+ agree_and_suspend:
+ title: "Sospendi utente"
+ description: "Accetta segnalazione e sospendi l'utente."
+ agree_and_silence:
+ title: "Silenzia utente"
+ description: "Accetta segnalazione e silenzia l'utente."
+ agree_and_restore:
+ title: "Ripristina messaggio"
+ description: "Ripristina il messaggio in modo che gli utenti possano vederlo."
+ agree_and_delete:
+ title: "Elimina messaggio"
+ description: "Elimina il messaggio in modo che gli utenti non possano vederlo."
+ delete_and_agree:
+ title: "Elimina messaggio"
+ disagree_and_restore:
+ title: "Rifiuta e ripristina il messaggio"
+ description: "Ripristina il messaggio in modo che tutti gli utenti possano vederlo."
+ disagree:
+ title: "Rifiuta"
+ ignore:
+ title: "Ignora"
+ direct_messages:
+ transcript_title: "Trascrizione dei messaggi precedenti in %{channel_name}"
+ transcript_body: "Per darti più contesto, abbiamo incluso una trascrizione dei messaggi precedenti in questa conversazione (fino a dieci):\n\n%{transcript}"
+ channel:
+ statuses:
+ read_only: "Sola lettura"
+ archived: "Archiviati"
+ closed: "Chiusi"
+ open: "Aperto"
+ archive:
+ first_post_raw: "Questo argomento è un archivio del canale di chat [%{channel_name}](%{channel_url})."
+ messages_moved:
+ one: "@%{acting_username} ha spostato un messaggio nel canale [%{channel_name}](%{first_moved_message_url})."
+ other: "@%{acting_username} ha spostato %{count} messaggi sul canale [%{channel_name}](%{first_moved_message_url})."
+ dm_title:
+ single_user: "%{user}"
+ multi_user: "%{users}"
+ multi_user_truncated: "%{users} e altri %{leftover}"
+ bookmarkable:
+ notification_title: "messaggio in %{channel_name}"
+ personal_chat: "chat personale"
+ onebox:
+ inline_to_message: "Messaggio n.%{message_id} per %{username} – n.%{chat_channel}"
+ inline_to_channel: "Chat n. %{chat_channel}"
+ inline_to_topic_channel: "Chat per l'argomento %{topic_title}"
+ x_members:
+ one: "%{count} membro"
+ other: "%{count} membri"
+ and_x_others:
+ one: "e %{count} altro"
+ other: "e %{count} altri"
+ discourse_push_notifications:
+ popup:
+ chat_mention:
+ direct: '%{username} ti ha menzionato in "%{channel}"'
+ other_type: '%{username} ha menzionato %{identifier} in "%{channel}"'
+ direct_message_chat_mention:
+ direct: "%{username} ti ha menzionato nella chat personale"
+ other_type: "%{username} ha menzionato %{identifier} nella chat personale"
+ new_chat_message: '%{username} ha inviato un messaggio in "%{channel}"'
+ new_direct_chat_message: "%{username} ha inviato un messaggio nella chat personale"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ title: Invia messaggio di chat
+ reviewable_score_types:
+ needs_review:
+ title: "Necessita di revisione"
+ notify_user:
+ chat_pm_title: 'Tuo messaggio di chat in "%{channel_name}"'
+ chat_pm_body: "%{link}\n\n%{message}"
+ notify_moderators:
+ chat_pm_title: 'Un messaggio di chat in "%{channel_name}" richiede l''attenzione del personale'
+ chat_pm_body: "%{link}\n\n%{message}"
+ reviewables:
+ reasons:
+ chat_message_queued_by_staff: "Un membro dello staff ritiene che questo messaggio di chat debba essere rivisto."
+ user_notifications:
+ chat_summary:
+ deleted_user: "Utente eliminato"
+ description:
+ one: "Hai un nuovo messaggio di chat"
+ other: "Hai nuovi messaggi di chat"
+ from: "%{site_name}"
+ subject:
+ direct_message:
+ one: "[%{email_prefix}] Nuovo messaggio da %{message_title}"
+ other: "[%{email_prefix}] Nuovi messaggi da %{message_title} e %{others}"
+ chat_channel:
+ one: "[%{email_prefix}] Nuovo messaggio in %{message_title}"
+ other: "[%{email_prefix}] Nuovi messaggi in %{message_title} e %{others}"
+ other_direct_message: "da %{message_title}"
+ others: "e %{count} altri"
+ unsubscribe: "Questo riepilogo della chat viene inviato da %{site_link} quando non ci sei. Modifica %{email_preferences_link} o %{unsubscribe_link} per annullare l'iscrizione."
+ unsubscribe_no_link: "Questo riepilogo della chat viene inviato da %{site_link} quando non ci sei. Modifica %{email_preferences_link}."
+ view_messages:
+ one: "Visualizza messaggio"
+ other: "Visualizza %{count} messaggi"
+ view_more:
+ one: "Visualizza %{count} altro messaggio"
+ other: "Visualizza altri %{count} messaggi"
+ your_chat_settings: "preferenza di frequenza e-mail della chat"
+ unsubscribe:
+ chat_summary:
+ select_title: "Imposta la frequenza delle e-mail di riepilogo della chat su:"
+ never: Mai
+ when_away: Solo quando non sono collegato
+ category:
+ cannot_delete:
+ has_chat_channels: "Impossibile eliminare questa categoria perché ha canali di chat."
diff --git a/plugins/chat/config/locales/server.ja.yml b/plugins/chat/config/locales/server.ja.yml
new file mode 100644
index 0000000000..ed2b822e2c
--- /dev/null
+++ b/plugins/chat/config/locales/server.ja.yml
@@ -0,0 +1,176 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+ja:
+ site_settings:
+ chat_allowed_groups: "これらのグループのユーザーがチャットできます。スタッフはいつでもチャットにアクセスできることに注意してください。"
+ chat_channel_retention_days: "通常のチャンネルのチャットメッセージは、この日数の間保持されます。メッセージを永久に保持するには、'0' に設定します。"
+ chat_dm_retention_days: "パーソナルチャットチャンネルのチャットメッセージは、この日数の間保持されます。メッセージを永久に保持するには、'0' に設定します。"
+ chat_auto_silence_duration: "チャットメッセージの作成速度制限を超えた場合にユーザーが投稿禁止になる分数。自動投稿禁止を無効にするには '0' に設定します。"
+ chat_allowed_messages_for_trust_level_0: "信頼レベル 0 のユーザーが 30 秒間に送信できるメッセージの件数。'0' に設定すると、制限が無効になります。"
+ chat_allowed_messages_for_other_trust_levels: "信頼レベル 1~4 のユーザーが 30 秒間に送信できるメッセージの件数。'0' に設定すると、制限が無効になります。"
+ chat_silence_user_sensitivity: "チャットで通報されたユーザーが自動的に投稿禁止にされる可能性。"
+ chat_auto_silence_from_flags_duration: "チャットメッセージの通報によって自動的に投稿禁止にされる場合に、ユーザーが投稿禁止になる分数。"
+ chat_default_channel_id: "ユーザーに、他のチャンネルの未読のメッセージまたはメンションがない場合に、デフォルトでオープンになるチャットチャンネル。"
+ chat_duplicate_message_sensitivity: "同じ送信者による重複したメッセージが短期間でブロックされる可能性。0~1.0 の 10 進数で、1.0 が最高の設定です(より短期間でより頻繁にメッセージをブロックします)。'0' に設定すると、重複メッセージが許可されます。"
+ chat_minimum_message_length: "チャットメッセージの最低文字数。"
+ chat_allow_uploads: "公開チャットチャンネルとダイレクトメッセージチャンネルでアップロードを許可します。"
+ chat_archive_destination_topic_status: "チャンネルのアーカイブが完了した後のアーカイブ先トピックのステータス。これは、アーカイブ先のトピックが既存のトピックではなく新しいトピックである場合にのみ適用されます。"
+ default_emoji_reactions: "チャットメッセージのデフォルトの絵文字リアクション。すぐにリアクションするための絵文字を最大 5 個追加できます。"
+ direct_message_enabled_groups: "これらのグループのユーザーがユーザー間のパーソナルチャットを作成することを許可します。注意: スタッフはいつでもパーソナルチャットを作成でき、ユーザーは作成権限のあるユーザーが開始したパーソナルチャットに返信できます。"
+ chat_message_flag_allowed_groups: "これらのグループのユーザーは、チャットメッセージを通報できます。"
+ errors:
+ chat_default_channel: "デフォルトのチャットチャンネルは公開チャンネルである必要があります。"
+ direct_message_enabled_groups_invalid: "この設定には、少なくとも 1 つのグループを指定する必要があります。スタッフ以外がダイレクトメッセージを送れないようにするには、スタッフグループを選択します。"
+ chat_upload_not_allowed_secure_uploads: "安全なアップロードのサイト設定が有効でない場合、チャットのアップロードは行えません。"
+ system_messages:
+ chat_channel_archive_complete:
+ title: "チャットチャンネルのアーカイブ完了"
+ subject_template: "チャットチャンネルのアーカイブが正常に完了しました"
+ text_body_template: |
+ チャットチャンネル **\#%{channel_name}** のアーカイブが正常に完了しました。メッセージはトピック [%{topic_title}](%{topic_url}) にコピーされました。
+ chat_channel_archive_failed:
+ title: "チャットチャンネルのアーカイブ失敗"
+ subject_template: "チャットチャンネルのアーカイブに失敗しました"
+ text_body_template: |
+ チャットチャンネル **\#%{channel_name}** のアーカイブに失敗しました。%{messages_archived} 件のメッセージがアーカイブされました。部分的にアーカイブされたメッセージは、トピック [%{topic_title}](%{topic_url}) にコピーされました。%{channel_url} よりチャンネルにアクセスして、再試行してください。
+ chat:
+ deleted_chat_username: 削除済み
+ errors:
+ channel_exists_for_category: "このカテゴリと名前のチャンネルはすでに存在します"
+ channel_new_message_disallowed: "チャンネルは %{status} です。新しいメッセージは送信できません"
+ channel_modify_message_disallowed: "チャンネルは %{status} です。メッセージの編集や削除は行えません"
+ user_cannot_send_message: "現在、メッセージを送信できません。"
+ rate_limit_exceeded: "30 秒間で送信できるチャットメッセージの件数制限を超えました"
+ auto_silence_from_flags: "ユーザーを投稿禁止にするのに十分なスコアで通報されたチャットメッセージ。"
+ channel_cannot_be_archived: "現在、チャンネルをアーカイブできません。アーカイブするには閉鎖されているかオープンである必要があります。"
+ duplicate_message: "同一のメッセージを最近投稿しました。"
+ delete_channel_failed: "チャンネルの削除に失敗しました。もう一度お試しください。"
+ minimum_length_not_met: "メッセージが短すぎます。最低 %{minimum} 文字が必要です。"
+ max_reactions_limit_reached: "このメッセージでは、新しいリアクションは許可されていません。"
+ message_move_invalid_channel: "移動元と移動先のチャンネルは公開チャンネルである必要があります。"
+ message_move_no_messages_found: "指定されたメッセージ ID を持つメッセージは見つかりませんでした。"
+ cant_update_direct_message_channel: "名前や説明と言ったダイレクトメッセージのチャンネルプロパティを更新できません。"
+ not_accepting_dms: "%{username} は現在、メッセージを受け付けていません。"
+ actor_ignoring_target_user: "%{username} を無視しているため、メッセージを送信できません。"
+ actor_muting_target_user: "%{username} をミュートしているため、メッセージを送信できません。"
+ actor_disallowed_dms: "ユーザーがあなたにプライベートメッセージやダイレクトメッセージを送信できないように選択しているため、新しいダイレクトメッセージを作成できません。"
+ actor_preventing_target_user_from_dm: "%{username} があなたにプライベートメッセージやダイレクトメッセージを送信できないように選択しているため、新しいダイレクトメッセージを作成できません。"
+ user_cannot_send_direct_messages: "ダイレクトメッセージを送信できません。"
+ reviewables:
+ message_already_handled: "ありがとうございます。ただ、このメッセージはすでにレビュー済みで、通報の必要がないと判断されています。"
+ actions:
+ agree:
+ title: "同意…"
+ agree_and_keep_message:
+ title: "メッセージを維持"
+ description: "通報に同意し、メッセージを未変更のままにします。"
+ agree_and_keep_deleted:
+ title: "メッセージの削除を維持"
+ description: "通報に同意し、メッセージを削除したままにします。"
+ agree_and_suspend:
+ title: "ユーザーを凍結"
+ description: "通報に同意し、ユーザーを凍結します。"
+ agree_and_silence:
+ title: "ユーザーを投稿禁止"
+ description: "通報に同意し、ユーザーを投稿禁止にします。"
+ agree_and_restore:
+ title: "メッセージを復元"
+ description: "ユーザーが閲覧できるようにメッセージを復元します。"
+ agree_and_delete:
+ title: "メッセージを削除"
+ description: "ユーザーが閲覧できないようにメッセージを削除します。"
+ delete_and_agree:
+ title: "メッセージを削除"
+ disagree_and_restore:
+ title: "同意せずにメッセージを復元"
+ description: "すべてのユーザーが閲覧できるようにメッセージを復元します。"
+ disagree:
+ title: "同意しない"
+ ignore:
+ title: "無視"
+ direct_messages:
+ transcript_title: "%{channel_name} の前のメッセージのトランスクリプト"
+ transcript_body: "より文脈を掴みやすいように、この会話の前のメッセージのトランスクリプトを含めました(最大 10 件)。\n\n%{transcript}"
+ channel:
+ statuses:
+ read_only: "読み取り専用"
+ archived: "アーカイブ済み"
+ closed: "閉鎖"
+ open: "オープン"
+ archive:
+ first_post_raw: "このトピックは、[%{channel_name}](%{channel_url}) チャットチャンネルのアーカイブです。"
+ messages_moved:
+ other: "@%{acting_username} が %{count} 件のメッセージを [%{channel_name}](%{first_moved_message_url}) チャンネルに移動しました。"
+ dm_title:
+ single_user: "%{user}"
+ multi_user: "%{users}"
+ multi_user_truncated: "%{users} および他 %{leftover} 人"
+ bookmarkable:
+ notification_title: "%{channel_name} のメッセージ"
+ personal_chat: "パーソナルチャット"
+ onebox:
+ inline_to_message: "%{username} によるメッセージ #%{message_id} - #%{chat_channel}"
+ inline_to_channel: "チャット #%{chat_channel}"
+ inline_to_topic_channel: "トピック %{topic_title} のチャット"
+ x_members:
+ other: "%{count} 人のメンバー"
+ and_x_others:
+ other: "および他 %{count} 人"
+ discourse_push_notifications:
+ popup:
+ chat_mention:
+ direct: '%{username} があなたを "%{channel}" でメンションしました'
+ other_type: '%{username} が %{identifier} を "%{channel}" でメンションしました'
+ direct_message_chat_mention:
+ direct: "%{username} がパーソナルチャットであなたをメンションしました"
+ other_type: "%{username} が %{identifier} をパーソナルチャットでメンションしました"
+ new_chat_message: '%{username} が "%{channel}" でメッセージを送信しました'
+ new_direct_chat_message: "%{username} がパーソナルチャットでメッセージを送信しました"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ title: チャットメッセージを送信する
+ reviewable_score_types:
+ needs_review:
+ title: "要レビュー"
+ notify_user:
+ chat_pm_title: '"%{channel_name}" のあなたのチャットメッセージ'
+ chat_pm_body: "%{link}\n\n%{message}"
+ notify_moderators:
+ chat_pm_title: '"%{channel_name}" のチャットメッセージには、スタッフの注意が必要です'
+ chat_pm_body: "%{link}\n\n%{message}"
+ reviewables:
+ reasons:
+ chat_message_queued_by_staff: "スタッフメンバーは、このチャットメッセージにレビューが必要だと考えています。"
+ user_notifications:
+ chat_summary:
+ deleted_user: "削除されたユーザー"
+ description:
+ other: "新しいチャットメッセージがあります"
+ from: "%{site_name}"
+ subject:
+ direct_message:
+ other: "[%{email_prefix}] %{message_title} と他 %{others} 件のチャットの新しいメッセージ"
+ chat_channel:
+ other: "[%{email_prefix}] %{message_title} と他 %{others} 件の新しいメッセージ"
+ other_direct_message: "%{message_title} から"
+ others: "他 %{count} 件"
+ unsubscribe: "このチャットの要約は、あなたが退席中のときに %{site_link} から送信されます。%{email_preferences_link} を変更するか、%{unsubscribe_link} から購読を停止します。"
+ unsubscribe_no_link: "このチャットの要約は、あなたが退席中のときに %{site_link} から送信されます。%{email_preferences_link} を変更します。"
+ view_messages:
+ other: "%{count} 件のメッセージを表示"
+ view_more:
+ other: "さらに %{count} 件のメッセージを表示"
+ your_chat_settings: "チャットメールの頻度設定"
+ unsubscribe:
+ chat_summary:
+ select_title: "チャット要約メールの頻度を設定:"
+ never: なし
+ when_away: 退席中の時のみ
+ category:
+ cannot_delete:
+ has_chat_channels: "このカテゴリにはチャットチャンネルがあるため削除できません。"
diff --git a/plugins/chat/config/locales/server.ko.yml b/plugins/chat/config/locales/server.ko.yml
new file mode 100644
index 0000000000..c8a3b106eb
--- /dev/null
+++ b/plugins/chat/config/locales/server.ko.yml
@@ -0,0 +1,40 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+ko:
+ chat:
+ deleted_chat_username: 삭제되었습니다
+ errors:
+ not_accepting_dms: "죄송합니다. %{username} 님이 당분간 메시지를 받지 않습니다."
+ reviewables:
+ actions:
+ agree:
+ title: "동의..."
+ agree_and_suspend:
+ title: "사용자 차단"
+ description: "플래그에 동의하고 사용자를 일시 중지하십시오."
+ agree_and_silence:
+ title: "글 작성 중지 사용자"
+ description: "신고에 동의하고 사용자를 쓰기 금지로 설정합니다."
+ disagree:
+ title: "동의하지 않음"
+ ignore:
+ title: "무시"
+ channel:
+ statuses:
+ closed: "닫힘"
+ open: "열기"
+ reviewable_score_types:
+ notify_user:
+ chat_pm_body: "%{link}\n\n%{message}"
+ notify_moderators:
+ chat_pm_body: "%{link}\n\n%{message}"
+ user_notifications:
+ chat_summary:
+ from: "%{site_name}"
+ unsubscribe:
+ chat_summary:
+ never: 알림 받지 않기
diff --git a/plugins/chat/config/locales/server.lt.yml b/plugins/chat/config/locales/server.lt.yml
new file mode 100644
index 0000000000..d6dc1b7c1e
--- /dev/null
+++ b/plugins/chat/config/locales/server.lt.yml
@@ -0,0 +1,43 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+lt:
+ chat:
+ deleted_chat_username: ištrintas
+ errors:
+ not_accepting_dms: "Atsiprašome, %{username} šiuo metu nepriima pranešimų."
+ reviewables:
+ actions:
+ agree:
+ title: "Sutinku..."
+ agree_and_suspend:
+ title: "Suspenduoti narį"
+ agree_and_silence:
+ title: "Nutildyti narį"
+ description: "Sutikti su pranešimu ir nutildyti narį."
+ disagree:
+ title: "Nesutinka"
+ ignore:
+ title: "Ignoruoti"
+ channel:
+ statuses:
+ closed: "Uždaryta"
+ open: "Atidaryti"
+ discourse_push_notifications:
+ popup:
+ chat_mention:
+ direct: '%{username} paminėjo tave "%{channel}"'
+ reviewable_score_types:
+ notify_user:
+ chat_pm_body: "%{link}\n\n%{message}"
+ notify_moderators:
+ chat_pm_body: "%{link}\n\n%{message}"
+ user_notifications:
+ chat_summary:
+ from: "%{site_name}"
+ unsubscribe:
+ chat_summary:
+ never: Niekada
diff --git a/plugins/chat/config/locales/server.lv.yml b/plugins/chat/config/locales/server.lv.yml
new file mode 100644
index 0000000000..9c059cb15e
--- /dev/null
+++ b/plugins/chat/config/locales/server.lv.yml
@@ -0,0 +1,28 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+lv:
+ chat:
+ deleted_chat_username: dzēsts
+ reviewables:
+ actions:
+ agree_and_suspend:
+ title: "Apturētie lietotāji"
+ disagree:
+ title: "Nepiekrist"
+ ignore:
+ title: "Ignorēt"
+ channel:
+ statuses:
+ closed: "Slēgts"
+ open: "Atvērt"
+ discourse_push_notifications:
+ popup:
+ chat_mention:
+ direct: '%{username} pieminēja jūs "%{channel}"'
+ unsubscribe:
+ chat_summary:
+ never: Nekad
diff --git a/plugins/chat/config/locales/server.nb_NO.yml b/plugins/chat/config/locales/server.nb_NO.yml
new file mode 100644
index 0000000000..bc8fd472cc
--- /dev/null
+++ b/plugins/chat/config/locales/server.nb_NO.yml
@@ -0,0 +1,39 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+nb_NO:
+ chat:
+ deleted_chat_username: slettet
+ errors:
+ not_accepting_dms: "Beklager, %{username} ønsker ikke å motta personlige meldinger for øyeblikket."
+ reviewables:
+ actions:
+ agree_and_suspend:
+ title: "Steng ute bruker"
+ description: "Si deg enig med rapportering og steng ute bruker. "
+ agree_and_silence:
+ title: "Demp bruker"
+ description: "Si deg enig med rapportering og demp bruker. "
+ disagree:
+ title: "Si deg uenig"
+ ignore:
+ title: "Ignorer"
+ channel:
+ statuses:
+ closed: "Lukket"
+ open: "Åpne"
+ discourse_push_notifications:
+ popup:
+ chat_mention:
+ direct: '%{username} nevnte deg i "%{channel}"'
+ reviewable_score_types:
+ notify_user:
+ chat_pm_body: "%{link}\n\n%{message}"
+ notify_moderators:
+ chat_pm_body: "%{link}\n\n%{message}"
+ unsubscribe:
+ chat_summary:
+ never: Aldri
diff --git a/plugins/chat/config/locales/server.nl.yml b/plugins/chat/config/locales/server.nl.yml
new file mode 100644
index 0000000000..87f4860a5d
--- /dev/null
+++ b/plugins/chat/config/locales/server.nl.yml
@@ -0,0 +1,40 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+nl:
+ chat:
+ deleted_chat_username: verwijderd
+ errors:
+ not_accepting_dms: "Sorry, %{username} accepteert momenteel geen berichten."
+ reviewables:
+ actions:
+ agree:
+ title: "Akkoord..."
+ agree_and_suspend:
+ title: "Gebruiker schorsen"
+ description: "Akkoord met markering en de gebruiker schorsen."
+ agree_and_silence:
+ title: "Gebruiker dempen"
+ description: "Akkoord met markering en de gebruiker dempen."
+ disagree:
+ title: "Niet akkoord"
+ ignore:
+ title: "Negeren"
+ channel:
+ statuses:
+ closed: "Gesloten"
+ open: "Openen"
+ reviewable_score_types:
+ notify_user:
+ chat_pm_body: "%{link}\n\n%{message}"
+ notify_moderators:
+ chat_pm_body: "%{link}\n\n%{message}"
+ user_notifications:
+ chat_summary:
+ from: "%{site_name}"
+ unsubscribe:
+ chat_summary:
+ never: Nooit
diff --git a/plugins/chat/config/locales/server.pl_PL.yml b/plugins/chat/config/locales/server.pl_PL.yml
new file mode 100644
index 0000000000..ea26c6afde
--- /dev/null
+++ b/plugins/chat/config/locales/server.pl_PL.yml
@@ -0,0 +1,106 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+pl_PL:
+ site_settings:
+ chat_enabled: "Włącz wtyczkę czatu."
+ chat:
+ deleted_chat_username: usunięte
+ errors:
+ channel_exists_for_category: "Istnieje już kanał dla tej kategorii i nazwy"
+ user_cannot_send_message: "W tej chwili nie możesz wysyłać wiadomości."
+ rate_limit_exceeded: "Przekroczono limit wiadomości na czacie, które można wysłać w ciągu 30 sekund"
+ max_reactions_limit_reached: "Nowe reakcje nie są dozwolone w tej wiadomości."
+ not_accepting_dms: "Przepraszamy, ale użytkownik %{username} nie akceptuje w tej chwili wiadomości "
+ reviewables:
+ actions:
+ agree:
+ title: "Zgadzam się..."
+ agree_and_keep_message:
+ title: "Zachowaj wiadomość"
+ agree_and_keep_deleted:
+ title: "Zachowaj wiadomość usuniętą"
+ agree_and_suspend:
+ title: "Zawieś użytkownika"
+ description: "Zgódź się z flagą i zawieś konto użytkownika."
+ agree_and_silence:
+ title: "Wycisz użytkownika"
+ description: "Zgódź się z flagą i wycisz użytkownika."
+ agree_and_restore:
+ title: "Przywróć wiadomość"
+ agree_and_delete:
+ title: "Usuń wiadomość"
+ delete_and_agree:
+ title: "Usuń wiadomość"
+ disagree:
+ title: "Wycofaj"
+ ignore:
+ title: "Ignoruj"
+ channel:
+ statuses:
+ read_only: "Tylko do odczytu"
+ archived: "Zarchiwizowany"
+ closed: "Zamknięta"
+ open: "Otwórz"
+ dm_title:
+ multi_user_truncated: "%{users} i %{leftover} innych"
+ bookmarkable:
+ notification_title: "wiadomość w %{channel_name}"
+ personal_chat: "czat osobisty"
+ onebox:
+ x_members:
+ one: "%{count} członek"
+ few: "%{count} członków"
+ many: "%{count} członków"
+ other: "%{count} członków"
+ and_x_others:
+ one: "i %{count} inny"
+ few: "i %{count} inne"
+ many: "i %{count} innych"
+ other: "i %{count} innych"
+ discourse_push_notifications:
+ popup:
+ chat_mention:
+ direct: '%{username} wspomniał(a) o Tobie w "%{channel}"'
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ title: Wyślij wiadomość na czacie
+ reviewable_score_types:
+ needs_review:
+ title: "Wymaga przeglądu"
+ notify_user:
+ chat_pm_title: 'Twoja wiadomość w kanale "%{channel_name}"'
+ chat_pm_body: "%{link}\n\n%{message}"
+ notify_moderators:
+ chat_pm_title: 'Wiadomość w kanale "%{channel_name}" wymaga uwagi personelu'
+ chat_pm_body: "%{link}\n\n%{message}"
+ reviewables:
+ reasons:
+ chat_message_queued_by_staff: "Członek personelu uważa, że ta wiadomość wymaga weryfikacji."
+ user_notifications:
+ chat_summary:
+ deleted_user: "Usunięty użytkownik"
+ description:
+ one: "Masz nową wiadomość na czacie"
+ few: "Masz nowe wiadomości na czacie"
+ many: "Masz nowe wiadomości na czacie"
+ other: "Masz nowe wiadomości na czacie"
+ from: "%{site_name}"
+ subject:
+ other_direct_message: "od %{message_title}"
+ others: "%{count} inni"
+ view_messages:
+ one: "Zobacz wiadomość"
+ few: "Zobacz %{count} wiadomości"
+ many: "Zobacz %{count} wiadomości"
+ other: "Zobacz %{count} wiadomości"
+ unsubscribe:
+ chat_summary:
+ never: Nigdy
+ category:
+ cannot_delete:
+ has_chat_channels: "Nie można usunąć tej kategorii, ponieważ zawiera ona kanały czatu."
diff --git a/plugins/chat/config/locales/server.pt.yml b/plugins/chat/config/locales/server.pt.yml
new file mode 100644
index 0000000000..7efeafb1ca
--- /dev/null
+++ b/plugins/chat/config/locales/server.pt.yml
@@ -0,0 +1,37 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+pt:
+ chat:
+ deleted_chat_username: eliminado
+ errors:
+ not_accepting_dms: "Desculpe, de momento, %{username} não está a aceitar mensagens."
+ reviewables:
+ actions:
+ agree_and_suspend:
+ title: "Utilizador Suspenso"
+ agree_and_silence:
+ title: "Silenciar Usuário"
+ disagree:
+ title: "Discordar"
+ ignore:
+ title: "Ignorar"
+ channel:
+ statuses:
+ closed: "Fechado"
+ open: "Abrir"
+ discourse_push_notifications:
+ popup:
+ chat_mention:
+ direct: '%{username} mencionou-o em "%{channel}"'
+ reviewable_score_types:
+ notify_user:
+ chat_pm_body: "%{link}\n\n%{message}"
+ notify_moderators:
+ chat_pm_body: "%{link}\n\n%{message}"
+ unsubscribe:
+ chat_summary:
+ never: Nunca
diff --git a/plugins/chat/config/locales/server.pt_BR.yml b/plugins/chat/config/locales/server.pt_BR.yml
new file mode 100644
index 0000000000..ef5e4b5086
--- /dev/null
+++ b/plugins/chat/config/locales/server.pt_BR.yml
@@ -0,0 +1,184 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+pt_BR:
+ site_settings:
+ chat_allowed_groups: "Os usuários desses grupos podem conversar. Observe que a equipe sempre pode acessar o chat."
+ chat_channel_retention_days: "Mensagens de chat em canais regulares serão mantidas por essa quantidade de dias. Defina para '0' para manter mensagens para sempre."
+ chat_dm_retention_days: "Mensagens de chat em canais pessoais serão mantidas por essa quantidade de dias. Defina para '0' para manter mensagens para sempre."
+ chat_auto_silence_duration: "Número de minutos pelos quais os usuários serão silenciados quando excederem o limite de criação de mensagens de chat. Defina como '0' para desativar o silenciamento automático."
+ chat_allowed_messages_for_trust_level_0: "Número de mensagens que os usuários de nível 0 de confiança podem enviar em 30 segundos. Defina como '0' para desativar o limite."
+ chat_allowed_messages_for_other_trust_levels: "Número de mensagens que os usuários de nível 1-4 de confiança podem enviar em 30 segundos. Defina como '0' para desativar o limite."
+ chat_silence_user_sensitivity: "A probabilidade de um usuário sinalizado no chat ser silenciado automaticamente."
+ chat_auto_silence_from_flags_duration: "Número de minutos pelos quais os usuários serão silenciados quando forem silenciados automaticamente devido a mensagens de chat sinalizadas."
+ chat_default_channel_id: "O canal de chat que será aberto por padrão quando um usuário não tiver mensagens não lidas ou menções em outros canais."
+ chat_duplicate_message_sensitivity: "A probabilidade de que uma mensagem duplicada do mesmo remetente seja bloqueada em um curto período. Número decimal entre 0 e 1.0, sendo 1.0 a configuração mais alta (bloqueia as mensagens com mais frequência em menos tempo). Defina como '0' para permitir mensagens duplicadas."
+ chat_minimum_message_length: "Número mínimo de caracteres para uma mensagem de chat."
+ chat_allow_uploads: "Permitir envios em canais de chat públicos e canais de mensagens diretas."
+ chat_archive_destination_topic_status: "O status que o tópico de destino deve ter quando um arquivo de canal for concluído. Isso só se aplica quando o tópico de destino for um tópico novo, não um preexistente."
+ default_emoji_reactions: "Reações de emoji padrão para mensagens do chat. Adicione até 5 emojis para uma reação rápida."
+ direct_message_enabled_groups: "Permitir que os usuários desses grupos criem chats pessoais de usuário para usuário. Observação: a equipe sempre pode criar chats pessoais e os usuários poderão responder aos chats pessoais iniciados por usuários que tenham permissão para criá-los."
+ chat_message_flag_allowed_groups: "Os usuários desses grupos podem sinalizar mensagens do chat."
+ errors:
+ chat_default_channel: "O canal de chat padrão deve ser um canal público."
+ direct_message_enabled_groups_invalid: "Você deve especificar pelo menos um grupo para esta configuração. Se você não quiser que ninguém, exceto a equipe, envie mensagens diretas, escolha o grupo da equipe."
+ chat_upload_not_allowed_secure_uploads: "Enviar arquivos no chat não é permitido quando a configuração do site de envios seguros estiver habilitada."
+ system_messages:
+ chat_channel_archive_complete:
+ title: "Arquivamento do Canal de Chat Concluído"
+ subject_template: "Arquivo do canal de chat concluído com sucesso"
+ text_body_template: |
+ O arquivamento do canal de chat **\#%{channel_name}** foi concluído com êxito. As mensagens foram copiadas para o tópico [%{topic_title}](%{topic_url}).
+ chat_channel_archive_failed:
+ title: "Falha no Arquivamento do Canal de Chat"
+ subject_template: "Falha no arquivamento do canal de chat"
+ text_body_template: |
+ Falha ao realizar o arquivamento do canal de chat **\#%{channel_name}**. %{messages_archived} mensagens foram arquivadas. As mensagens parcialmente arquivadas foram copiadas para o tópico [%{topic_title}](%{topic_url}). Visite o canal em %{channel_url} para tentar novamente.
+ chat:
+ deleted_chat_username: excluído
+ errors:
+ channel_exists_for_category: "Já existe um canal para este nome e categoria"
+ channel_new_message_disallowed: "O canal está %{status}, nenhuma mensagem nova pode ser enviada"
+ channel_modify_message_disallowed: "O canal está %{status}, nenhuma mensagem pode ser editada ou excluída"
+ user_cannot_send_message: "Você não pode enviar mensagens neste momento."
+ rate_limit_exceeded: "Excedeu o limite de mensagens de chat que podem ser enviadas em 30 segundos"
+ auto_silence_from_flags: "Mensagem de chat sinalizada com pontuação alta o suficiente para silenciar o usuário."
+ channel_cannot_be_archived: "O canal não pode ser arquivado no momento, ele deve estar fechado ou aberto para arquivar."
+ duplicate_message: "Você postou uma mensagem idêntica muito recentemente."
+ delete_channel_failed: "Falha ao excluir canal. Tente novamente."
+ minimum_length_not_met: "A mensagem é muito curta, deve ter no mínimo %{minimum} caracteres."
+ max_reactions_limit_reached: "Novas reações não são permitidas nesta mensagem."
+ message_move_invalid_channel: "O canal de origem e de destino deve ser canais públicos."
+ message_move_no_messages_found: "Nenhuma mensagem foi encontrada com os IDs de mensagem fornecidos."
+ cant_update_direct_message_channel: "As propriedades do canal de mensagem direta, como nome e descrição, não podem ser atualizadas."
+ not_accepting_dms: "Desculpe, %{username} não está aceitando mensagens no momento."
+ actor_ignoring_target_user: "Você está ignorando %{username}, então você não pode enviar mensagens para ele(a)."
+ actor_muting_target_user: "Você está silenciando %{username}, então você não pode enviar mensagens para ele(a)."
+ actor_disallowed_dms: "Você optou por impedir que os usuários lhe enviem mensagens privadas e diretas, portanto, você não pode criar novas mensagens diretas."
+ actor_preventing_target_user_from_dm: "Você optou por impedir que %{username} lhe envie mensagens privadas e diretas, portanto, você não pode criar novas mensagens diretas para ele(a)."
+ user_cannot_send_direct_messages: "Desculpe, você não pode enviar mensagens diretas."
+ reviewables:
+ message_already_handled: "Obrigado, mas já analisamos esta mensagem e determinamos que ela não precisa ser sinalizada novamente."
+ actions:
+ agree:
+ title: "Concordo..."
+ agree_and_keep_message:
+ title: "Manter mensagem"
+ description: "Concorde com a sinalização e mantenha a mensagem inalterada."
+ agree_and_keep_deleted:
+ title: "Manter mensagem excluída"
+ description: "Concorde com a sinalização e mantenha a mensagem excluída."
+ agree_and_suspend:
+ title: "Suspender usuário(s)"
+ description: "Concorde com a sinalização e suspenda o usuário(a)."
+ agree_and_silence:
+ title: "Silenciar usuário(a)"
+ description: "Concorde com a sinalização e silencie o usuário(a)."
+ agree_and_restore:
+ title: "Restaurar mensagem"
+ description: "Restaure a mensagem para que os(as) usuários(as) possam vê-las."
+ agree_and_delete:
+ title: "Excluir mensagem"
+ description: "Exclua a mensagem para que os(as) usuários(as) não possam vê-las."
+ delete_and_agree:
+ title: "Excluir mensagem"
+ disagree_and_restore:
+ title: "Não concordar e restaurar mensagem"
+ description: "Restaure a mensagem para que todos(as) os(as) usuários(as) possam vê-las."
+ disagree:
+ title: "Discordar"
+ ignore:
+ title: "Ignorar"
+ direct_messages:
+ transcript_title: "Transcrição de mensagens anteriores em %{channel_name}"
+ transcript_body: "Para dar mais contexto, incluímos uma transcrição das mensagens anteriores nesta conversa (até dez):\n\n%{transcript}"
+ channel:
+ statuses:
+ read_only: "Somente leitura"
+ archived: "Arquivados"
+ closed: "Fechados"
+ open: "Aberto"
+ archive:
+ first_post_raw: "Este tópico é um arquivo do canal do chat [%{channel_name}](%{channel_url})."
+ messages_moved:
+ one: "@%{acting_username} moveu uma mensagem para o canal [%{channel_name}](%{first_moved_message_url})."
+ other: "@%{acting_username} moveu %{count} mensagens para o canal [%{channel_name}](%{first_moved_message_url})."
+ dm_title:
+ single_user: "%{user}"
+ multi_user: "%{users}"
+ multi_user_truncated: "%{users} e %{leftover} outros"
+ bookmarkable:
+ notification_title: "mensagem em %{channel_name}"
+ personal_chat: "chat pessoal"
+ onebox:
+ inline_to_message: "Mensagem #%{message_id} por %{username} – #%{chat_channel}"
+ inline_to_channel: "Chat #%{chat_channel}"
+ inline_to_topic_channel: "Chat do Tópico %{topic_title}"
+ x_members:
+ one: "%{count} membro"
+ other: "%{count} membros"
+ and_x_others:
+ one: "e %{count} outro"
+ other: "e %{count} outros"
+ discourse_push_notifications:
+ popup:
+ chat_mention:
+ direct: '%{username} mencionou você em "%{channel}"'
+ other_type: '%{username} mencionou %{identifier} em "%{channel}"'
+ direct_message_chat_mention:
+ direct: "%{username} mencionou você no chat pessoal"
+ other_type: "%{username} mencionou %{identifier} no chat pessoal"
+ new_chat_message: '%{username} enviou uma mensagem em "%{channel}"'
+ new_direct_chat_message: "%{username} enviou uma mensagem no chat"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ title: Enviar mensagem de chat
+ reviewable_score_types:
+ needs_review:
+ title: "Precisa de revisão"
+ notify_user:
+ chat_pm_title: 'Nova mensagem de chat em "%{channel_name}"'
+ chat_pm_body: "%{link}\n\n%{message}"
+ notify_moderators:
+ chat_pm_title: 'Uma mensagem de chat em "%{channel_name}" requer atenção da equipe'
+ chat_pm_body: "%{link}\n\n%{message}"
+ reviewables:
+ reasons:
+ chat_message_queued_by_staff: "Um membro da equipe acha que esta mensagem do chat precisa ser revisada."
+ user_notifications:
+ chat_summary:
+ deleted_user: "Usuário excluído"
+ description:
+ one: "Você tem uma nova mensagem de chat"
+ other: "Você tem novas mensagens de chat"
+ from: "%{site_name}"
+ subject:
+ direct_message:
+ one: "[%{email_prefix}] Nova mensagem de %{message_title}"
+ other: "[%{email_prefix}] Novas mensagens de %{message_title} e %{others}"
+ chat_channel:
+ one: "[%{email_prefix}] Nova mensagem de %{message_title}"
+ other: "[%{email_prefix}] Novas mensagens em %{message_title} e %{others}"
+ other_direct_message: "de %{message_title}"
+ others: "outros %{count}"
+ unsubscribe: "Este resumo do chat será enviado de %{site_link} quando você estiver ausente. Altere seu %{email_preferences_link}, ou %{unsubscribe_link} para cancelar a inscrição."
+ unsubscribe_no_link: "Este resumo do chat será enviado de %{site_link} quando você estiver ausente. Altere seu %{email_preferences_link}."
+ view_messages:
+ one: "Ver mensagem"
+ other: "Ver %{count} mensagens"
+ view_more:
+ one: "Ver mais %{count} mensagens"
+ other: "Ver mais %{count} mensagens"
+ your_chat_settings: "preferência de frequência de e-mail do chat"
+ unsubscribe:
+ chat_summary:
+ select_title: "Defina a frequência de e-mails de resumo do chat para:"
+ never: Nunca
+ when_away: Só quando estiver ausente
+ category:
+ cannot_delete:
+ has_chat_channels: "Não é possível excluir esta categoria, porque ela tem canais de chat."
diff --git a/plugins/chat/config/locales/server.ro.yml b/plugins/chat/config/locales/server.ro.yml
new file mode 100644
index 0000000000..f6d61b6885
--- /dev/null
+++ b/plugins/chat/config/locales/server.ro.yml
@@ -0,0 +1,37 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+ro:
+ chat:
+ deleted_chat_username: șters
+ errors:
+ not_accepting_dms: "%{username} nu acceptă mesaje în acest moment."
+ reviewables:
+ actions:
+ agree_and_suspend:
+ title: "Suspendă Utilizator"
+ agree_and_silence:
+ title: "Suspendă utilizatorul"
+ disagree:
+ title: "Nu sunt de acord"
+ ignore:
+ title: "Ignoră"
+ channel:
+ statuses:
+ closed: "Închis"
+ open: "Deschide sondajul"
+ discourse_push_notifications:
+ popup:
+ chat_mention:
+ direct: '%{username} te-a menționat în discuția "%{channel}"'
+ reviewable_score_types:
+ notify_user:
+ chat_pm_body: "%{link}\n\n%{message}"
+ notify_moderators:
+ chat_pm_body: "%{link}\n\n%{message}"
+ unsubscribe:
+ chat_summary:
+ never: Niciodată
diff --git a/plugins/chat/config/locales/server.ru.yml b/plugins/chat/config/locales/server.ru.yml
new file mode 100644
index 0000000000..bb8a3d12c1
--- /dev/null
+++ b/plugins/chat/config/locales/server.ru.yml
@@ -0,0 +1,200 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+ru:
+ site_settings:
+ chat_allowed_groups: "Пользователи в этих группах могут общаться в чате. Обратите внимание, что сотрудники всегда могут получить доступ к чату."
+ chat_channel_retention_days: "Сообщения чата в обычных каналах будут храниться указанное здесь количество дней. Установите значение в '0', чтобы сохранять сообщения навсегда."
+ chat_dm_retention_days: "Сообщения чата в личных каналах чата будут храниться указанное здесь количество дней. Установите значение в '0', чтобы сохранять сообщения навсегда."
+ chat_auto_silence_duration: "Количество минут, в течение которых пользователи будут заблокированы, если они превысят лимит скорости создания сообщений в чате. Установите значение в '0', чтобы отключить автоблокировку."
+ chat_allowed_messages_for_trust_level_0: "Количество сообщений, которые пользователи с уровнем доверия '0' могут отправлять в течение 30 секунд. Установите значение в '0', чтобы отключить ограничение."
+ chat_allowed_messages_for_other_trust_levels: "Количество сообщений, которые пользователи с уровнем доверия от '1' до '4' могут отправлять в течение 30 секунд. Установите значение в '0', чтобы отключить ограничение."
+ chat_silence_user_sensitivity: "Вероятность того, что пользователь, на которого поступила жалоба, будет автоматически заблокирован."
+ chat_auto_silence_from_flags_duration: "Количество минут, в течение которых пользователи будут заблокированы, если на их сообщения поступают жалобы."
+ chat_default_channel_id: "Канал чата, который будет открываться по умолчанию, когда у пользователя нет непрочитанных сообщений или упоминаний в других каналах."
+ chat_duplicate_message_sensitivity: "Вероятность того, что дубликат сообщения от одного и того же отправителя будет заблокирован через короткий промежуток времени. Десятичное число от '0' до '1.0', где '1.0' — блокирует сообщения наиболее часто за короткий промежуток времени, а '0' - разрешает дублирование сообщений."
+ chat_minimum_message_length: "Минимальное количество символов при создании сообщения чата."
+ chat_allow_uploads: "Разрешить загрузку в общедоступных каналах чата и каналах прямых сообщений."
+ chat_archive_destination_topic_status: "Статус, который должен быть присвоен теме назначения после завершения архивирования канала. Присваивается только в том случае, если целевой темой является новая тема, а не существующая."
+ default_emoji_reactions: "Стандартные эмодзи чата. Можно добавить до 5 смайликов."
+ direct_message_enabled_groups: "Разрешить пользователям в этих группах создавать личные чаты между пользователями. Примечание. Сотрудники всегда могут создавать личные чаты, а пользователи смогут отвечать на личные чаты, инициированные пользователями, имеющими разрешение на их создание."
+ chat_message_flag_allowed_groups: "Пользователям этих групп разрешено жаловаться на сообщения в чате."
+ errors:
+ chat_default_channel: "Канал чата по умолчанию должен быть общедоступным."
+ direct_message_enabled_groups_invalid: "Для этого параметра необходимо указать хотя бы одну группу. Если вы не хотите, чтобы кто-либо, кроме сотрудников, отправлял прямые сообщения, выберите группу сотрудников."
+ chat_upload_not_allowed_secure_uploads: "Загрузка в чат запрещена, если включено ограничение доступа к загружаемому контенту."
+ system_messages:
+ chat_channel_archive_complete:
+ title: "Архивация канала завершена"
+ subject_template: "Архивация канала успешно завершена"
+ text_body_template: |
+ Архивация канала **\#%{channel_name}** успешно завершена. Сообщения были скопированы в тему [%{topic_title}](%{topic_url}).
+ chat_channel_archive_failed:
+ title: "Не удалось заархивировать канал"
+ subject_template: "Не удалось заархивировать канал"
+ text_body_template: |
+ Не удалось заархивировать канал **\#%{channel_name}**. Сообщения %{messages_archived} были заархивированы. Частично заархивированные сообщения были скопированы в тему [%{topic_title}](%{topic_url}). Посетите канал %{channel_url} и повторите попытку.
+ chat:
+ deleted_chat_username: удалён
+ errors:
+ channel_exists_for_category: "Канал для этого раздела уже существует"
+ channel_new_message_disallowed: "Канал %{status}, в него не могут быть отправлены новые сообщения"
+ channel_modify_message_disallowed: "Канал %{status}, существующие сообщения не могут быть отредактированы или удалены"
+ user_cannot_send_message: "В настоящее время вы не можете отправлять сообщения."
+ rate_limit_exceeded: "Превышен лимит сообщений, которые могут быть отправлены в течение 30 секунд"
+ auto_silence_from_flags: "На сообщение поступило большое количество жалоб, и пользователь был заблокирован."
+ channel_cannot_be_archived: "Канал в данный момент не может быть заархивирован, он должен быть либо закрыт, либо открыт для архивации."
+ duplicate_message: "Вы отправляете одно и то же сообщение слишком часто."
+ delete_channel_failed: "Не удалось удалить канал, попробуйте ещё раз."
+ minimum_length_not_met: "Сообщение слишком короткое, оно должно содержать не менее %{minimum} символов."
+ max_reactions_limit_reached: "Новые реакции на это сообщение запрещены."
+ message_move_invalid_channel: "Исходный и целевой каналы должны быть общедоступными."
+ message_move_no_messages_found: "Не найдено сообщений с указанными идентификаторами сообщений."
+ cant_update_direct_message_channel: "Такие свойства канала как название и описание, не могут быть обновлены."
+ not_accepting_dms: "Извините, пользователь %{username} в данный момент не принимает личные \nсообщения."
+ actor_ignoring_target_user: "Вы игнорируете %{username}, поэтому не можете отправлять им личные сообщения."
+ actor_muting_target_user: "Вы отключили все уведомления от %{username}, поэтому вы не можете отправлять им личные сообщения."
+ actor_disallowed_dms: "Вы решили запретить пользователям отправлять вам личные и прямые сообщения чата, поэтому вы не можете создавать новые прямые сообщения."
+ actor_preventing_target_user_from_dm: "Вы решили запретить %{username} отправлять вам личные и прямые сообщения чата, поэтому вы не можете создавать для них новые прямые сообщения."
+ user_cannot_send_direct_messages: "К сожалению, вы не можете отправлять прямые сообщения."
+ reviewables:
+ message_already_handled: "Спасибо, но мы уже рассмотрели жалобу на это сообщение, поэтому жаловаться на него снова нет необходимости."
+ actions:
+ agree:
+ title: "Согласиться..."
+ agree_and_keep_message:
+ title: "Оставить сообщение"
+ description: "Согласиться с жалобой и оставить сообщение без изменений."
+ agree_and_keep_deleted:
+ title: "Оставить сообщение уделённым"
+ description: "Согласиться с жалобой и оставить сообщение удалённым."
+ agree_and_suspend:
+ title: "Заморозить пользователя"
+ description: "Согласиться с жалобой и заморозить пользователя."
+ agree_and_silence:
+ title: "Pf,kjrbhjdfnm gjkmpjdfntkz"
+ description: "Согласиться с жалобой и блокировать пользователя."
+ agree_and_restore:
+ title: "Восстановить сообщение"
+ description: "Восстановить сообщение, чтобы пользователи могли его видеть."
+ agree_and_delete:
+ title: "Удалить сообщение"
+ description: "Удалить сообщение, чтобы пользователи не могли его видеть."
+ delete_and_agree:
+ title: "Удалить сообщение"
+ disagree_and_restore:
+ title: "Отклонить жалобу и восстановить сообщение"
+ description: "Восстановить сообщение, чтобы все пользователи могли его видеть."
+ disagree:
+ title: "Отклонить"
+ ignore:
+ title: "Игнорировать"
+ direct_messages:
+ transcript_title: "Содержимое предыдущих сообщений в канале %{channel_name}"
+ transcript_body: "Чтобы дать больше контекста, мы отображаем содержимое предыдущих сообщений этой беседы (до десяти):\n\n%{transcript}"
+ channel:
+ statuses:
+ read_only: "Только для чтения"
+ archived: "Архивные"
+ closed: "Закрытые"
+ open: "Открыт"
+ archive:
+ first_post_raw: "Эта тема является архивом канала [%{channel_name}](%{channel_url})."
+ messages_moved:
+ one: "Пользователь @%{acting_username} переместил сообщение в канал [%{channel_name}](%{first_moved_message_url})."
+ few: "Пользователь @%{acting_username} переместил %{count} сообщения в канал [%{channel_name}](%{first_moved_message_url})."
+ many: "Пользователь @%{acting_username} переместил %{count} сообщений в канал [%{channel_name}](%{first_moved_message_url})."
+ other: "Пользователь @%{acting_username} переместил %{count} сообщений в канал [%{channel_name}](%{first_moved_message_url})."
+ dm_title:
+ single_user: "%{user}"
+ multi_user: "%{users}"
+ multi_user_truncated: "%{users} и ещё %{leftover}"
+ bookmarkable:
+ notification_title: "Сообщение в канале %{channel_name}"
+ personal_chat: "личный чат"
+ onebox:
+ inline_to_message: "Сообщение №%{message_id} от пользователя %{username} — №%{chat_channel}"
+ inline_to_channel: "Чат №%{chat_channel}"
+ inline_to_topic_channel: "Чат по теме %{topic_title}"
+ x_members:
+ one: "%{count} участник"
+ few: "%{count} участника"
+ many: "%{count} участников"
+ other: "%{count} участников"
+ and_x_others:
+ one: "и ещё %{count}"
+ few: "и ещё %{count}"
+ many: "и ещё %{count}"
+ other: "и ещё %{count}"
+ discourse_push_notifications:
+ popup:
+ chat_mention:
+ direct: 'Пользователь %{username} упомянул вас на канале "%{channel}"'
+ other_type: 'Пользователь %{username} упомянул %{identifier} в канале "%{channel}"'
+ direct_message_chat_mention:
+ direct: "Пользователь %{username} упомянул вас в личном чате"
+ other_type: "Пользователь %{username} упомянул %{identifier} в личном чате"
+ new_chat_message: 'Пользователь %{username} отправил сообщение на канале "%{channel}"'
+ new_direct_chat_message: "Пользователь %{username} отправил сообщение в личный чат"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ title: Отправить сообщение в чат
+ reviewable_score_types:
+ needs_review:
+ title: "Требуется проверка"
+ notify_user:
+ chat_pm_title: 'Ваше сообщение в канале ''%{channel_name}'''
+ chat_pm_body: "%{link}\n\n%{message}"
+ notify_moderators:
+ chat_pm_title: 'Сообщение в канале ''%{channel_name}'' требует внимания модератора'
+ chat_pm_body: "%{link}\n\n%{message}"
+ reviewables:
+ reasons:
+ chat_message_queued_by_staff: "Сотрудник считает, что это сообщение должно быть отправлено на премодерацию."
+ user_notifications:
+ chat_summary:
+ deleted_user: "Удалённый пользователь"
+ description:
+ one: "У вас в чате одно новое сообщение"
+ few: "У вас в чате есть новые сообщения"
+ many: "У вас в чате есть новые сообщения"
+ other: "У вас в чате есть новые сообщения"
+ from: "%{site_name}"
+ subject:
+ direct_message:
+ one: "[%{email_prefix}] Новое сообщение от %{message_title}"
+ few: "[%{email_prefix}] Новые сообщения от %{message_title} и %{others}"
+ many: "[%{email_prefix}] Новые сообщения от %{message_title} и %{others}"
+ other: "[%{email_prefix}] Новые сообщения от %{message_title} и %{others}"
+ chat_channel:
+ one: "[%{email_prefix}] Новое сообщение в %{message_title}"
+ few: "[%{email_prefix}] Новые сообщения в %{message_title} и %{others}"
+ many: "[%{email_prefix}] Новые сообщения в %{message_title} и %{others}"
+ other: "[%{email_prefix}] Новые сообщения в %{message_title} и %{others}"
+ other_direct_message: "от %{message_title}"
+ others: "%{count} других"
+ unsubscribe: "Этот дайджест чата отправляется с сайта %{site_link} в период вашего отсутствия на форуме. Для отмены подписки измените %{email_preferences_link} или %{unsubscribe_link}."
+ unsubscribe_no_link: "Этот дайджест чата рассылается с сайта %{site_link} в период вашего отсутствия на форуме. Настройка рассылки: %{email_preferences_link}."
+ view_messages:
+ one: "Посмотреть %{count} сообщение"
+ few: "Посмотреть %{count} сообщения"
+ many: "Посмотреть %{count} сообщений"
+ other: "Посмотреть %{count} сообщений"
+ view_more:
+ one: "Посмотреть ещё %{count} сообщение"
+ few: "Посмотреть ещё %{count} сообщения"
+ many: "Посмотреть ещё %{count} сообщений"
+ other: "Посмотреть ещё %{count} сообщений"
+ your_chat_settings: "Настройка частоты рассылки дайджеста чата"
+ unsubscribe:
+ chat_summary:
+ select_title: "Настройте частоту получения электронных писем с дайджестами чата:"
+ never: Никогда
+ when_away: Если вы находитесь офлайн
+ category:
+ cannot_delete:
+ has_chat_channels: "Невозможно удалить этот раздел, поскольку в нём есть каналы чата."
diff --git a/plugins/chat/config/locales/server.sk.yml b/plugins/chat/config/locales/server.sk.yml
new file mode 100644
index 0000000000..7ae7c9dd95
--- /dev/null
+++ b/plugins/chat/config/locales/server.sk.yml
@@ -0,0 +1,33 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+sk:
+ chat:
+ deleted_chat_username: vymazané
+ reviewables:
+ actions:
+ agree_and_suspend:
+ title: "Suspendovaný užívateľ"
+ agree_and_silence:
+ title: "Tichý užívateľ"
+ disagree:
+ title: "Nesúhlasiť"
+ channel:
+ statuses:
+ closed: "Zatvorené"
+ open: "Zahájiť"
+ discourse_push_notifications:
+ popup:
+ chat_mention:
+ direct: '%{username} Vás zmienil v "%{channel}"'
+ reviewable_score_types:
+ notify_user:
+ chat_pm_body: "%{link}\n\n%{message}"
+ notify_moderators:
+ chat_pm_body: "%{link}\n\n%{message}"
+ unsubscribe:
+ chat_summary:
+ never: Nikdy
diff --git a/plugins/chat/config/locales/server.sl.yml b/plugins/chat/config/locales/server.sl.yml
new file mode 100644
index 0000000000..02225d5911
--- /dev/null
+++ b/plugins/chat/config/locales/server.sl.yml
@@ -0,0 +1,39 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+sl:
+ chat:
+ deleted_chat_username: izbrisano
+ reviewables:
+ actions:
+ agree:
+ title: "Strinjam se..."
+ agree_and_suspend:
+ title: "Suspendiraj uporabnika"
+ description: "Strinjam se z prijavo in suspendiraj uporabnika."
+ agree_and_silence:
+ title: "Utišaj uporabnika"
+ description: "Potrdi prijavo in utišaj uporabnika"
+ disagree:
+ title: "Se ne strinjam"
+ ignore:
+ title: "Prezri"
+ channel:
+ statuses:
+ closed: "Zaprto"
+ open: "Odpri"
+ discourse_push_notifications:
+ popup:
+ chat_mention:
+ direct: '%{username} vas je omenil v "%{channel}"'
+ reviewable_score_types:
+ notify_user:
+ chat_pm_body: "%{link}\n\n%{message}"
+ notify_moderators:
+ chat_pm_body: "%{link}\n\n%{message}"
+ unsubscribe:
+ chat_summary:
+ never: Nikoli
diff --git a/plugins/chat/config/locales/server.sq.yml b/plugins/chat/config/locales/server.sq.yml
new file mode 100644
index 0000000000..3b2bd872fb
--- /dev/null
+++ b/plugins/chat/config/locales/server.sq.yml
@@ -0,0 +1,30 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+sq:
+ chat:
+ deleted_chat_username: deleted
+ reviewables:
+ actions:
+ agree_and_suspend:
+ title: "Pezullo anëtarin"
+ disagree:
+ title: "Jo dakord"
+ channel:
+ statuses:
+ open: "Fillo"
+ discourse_push_notifications:
+ popup:
+ chat_mention:
+ direct: '%{username} ju përmendi në "%{channel}"'
+ reviewable_score_types:
+ notify_user:
+ chat_pm_body: "%{link}\n\n%{message}"
+ notify_moderators:
+ chat_pm_body: "%{link}\n\n%{message}"
+ unsubscribe:
+ chat_summary:
+ never: Asnjëherë
diff --git a/plugins/chat/config/locales/server.sr.yml b/plugins/chat/config/locales/server.sr.yml
new file mode 100644
index 0000000000..7da3cd0571
--- /dev/null
+++ b/plugins/chat/config/locales/server.sr.yml
@@ -0,0 +1,22 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+sr:
+ chat:
+ errors:
+ not_accepting_dms: "Žao nam je, %{username} trenutno ne prihvata privatne poruke."
+ reviewables:
+ actions:
+ agree_and_suspend:
+ title: "Suspenduj Korisnika"
+ disagree:
+ title: "Odbaci"
+ channel:
+ statuses:
+ open: "Otvori"
+ unsubscribe:
+ chat_summary:
+ never: Nikad
diff --git a/plugins/chat/config/locales/server.sv.yml b/plugins/chat/config/locales/server.sv.yml
new file mode 100644
index 0000000000..0e5926e1cb
--- /dev/null
+++ b/plugins/chat/config/locales/server.sv.yml
@@ -0,0 +1,185 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+sv:
+ site_settings:
+ chat_enabled: "Aktivera chattillägget."
+ chat_allowed_groups: "Användare i dessa grupper kan chatta. Observera att personalen alltid har tillgång till chatten."
+ chat_channel_retention_days: "Chattmeddelanden i ordinarie kanaler kommer att behållas i så här många dagar. Sätt till '0' för att behålla meddelanden för alltid."
+ chat_dm_retention_days: "Chattmeddelanden i personliga chattkanaler kommer att behållas i så här många dagar. Sätt till '0' för att behålla meddelanden för alltid."
+ chat_auto_silence_duration: "Antal minuter som användare kommer att tystas när de överskrider antalsgränsen för skapande av chattmeddelanden. Ställ in på '0' för att inaktivera automatisk tystning."
+ chat_allowed_messages_for_trust_level_0: "Antal meddelanden som användare på förtroendenivå 0 får skicka inom 30 sekunder. Ange '0' för att inaktivera gränsen."
+ chat_allowed_messages_for_other_trust_levels: "Antal meddelanden som användare med förtroendenivå 1-4 får skicka inom 30 sekunder. Ange '0' för att inaktivera gränsen."
+ chat_silence_user_sensitivity: "Sannolikheten för att en användare som flaggas i chatten automatiskt tystas."
+ chat_auto_silence_from_flags_duration: "Antal minuter som användarna tystas i när de automatiskt tystas på grund av markerade chattmeddelanden."
+ chat_default_channel_id: "Chattkanalen som öppnas som standard när en användare inte har olästa meddelanden eller omnämnanden i andra kanaler."
+ chat_duplicate_message_sensitivity: "Sannolikheten att ett duplicerat meddelande från samma avsändare blockeras inom en kort tidsperiod. Decimaltal mellan 0 och 1,0, där 1,0 är den högsta inställningen (blockerar meddelanden oftare på kortare tid). Ställ in `0` för att tillåta dubbletter av meddelanden."
+ chat_minimum_message_length: "Minsta antal tecken för ett chattmeddelande."
+ chat_allow_uploads: "Tillåt uppladdningar i offentliga chattkanaler och direktmeddelandekanaler."
+ chat_archive_destination_topic_status: "Den status som målämnet ska ha när ett kanalarkiv är slutfört. Detta gäller endast när målämnet är ett nytt ämne, inte ett befintligt."
+ default_emoji_reactions: "Standardval av emoji-reaktioner för chattmeddelanden. Lägg till upp till 5 emojis som snabbval."
+ direct_message_enabled_groups: "Tillåt användare inom dessa grupper att skapa personliga chattar från användare till användare. Obs: personal kan alltid skapa personliga chattar och användare kommer att kunna svara på personliga chattar som initieras av användare som har tillstånd att skapa dem."
+ chat_message_flag_allowed_groups: "Användare i dessa grupper får flagga chattmeddelanden."
+ errors:
+ chat_default_channel: "Standardchattkanalen måste vara en offentlig kanal."
+ direct_message_enabled_groups_invalid: "Du måste ange minst en grupp för den här inställningen. Om du inte vill att någon förutom personal ska skicka direktmeddelanden, välj personalgrupp."
+ chat_upload_not_allowed_secure_uploads: "Uppladdningar via chatt tillåts inte när inställningen för säkra uppladdningar är aktiverad."
+ system_messages:
+ chat_channel_archive_complete:
+ title: "Arkivering av chattkanalen är färdigt"
+ subject_template: "Arkivering av chattkanalen slutfördes framgångsrikt"
+ text_body_template: |
+ Arkivering av chattkanalen **\#%{channel_name}** har slutförts. Meddelandena har kopierats till ämnet [%{topic_title}](%{topic_url}).
+ chat_channel_archive_failed:
+ title: "Arkivering av chattkanalen misslyckades"
+ subject_template: "Arkivering av chattkanalen misslyckades"
+ text_body_template: |
+ Arkivering av chatt kanalen **\#%{channel_name}** misslyckades. %{messages_archived} meddelanden har arkiverats. Delvis arkiverade meddelanden kopierades till ämnet [%{topic_title}](%{topic_url}). Besök kanalen på %{channel_url} för att försöka igen.
+ chat:
+ deleted_chat_username: raderad
+ errors:
+ channel_exists_for_category: "En kanal finns redan för denna kategori och namn"
+ channel_new_message_disallowed: "Kanalen är %{status}, inga nya meddelanden kan skickas"
+ channel_modify_message_disallowed: "Kanalen är %{status}, inga meddelanden kan redigeras eller tas bort"
+ user_cannot_send_message: "Du kan inte skicka meddelanden just nu."
+ rate_limit_exceeded: "Överskred gränsen för chattmeddelanden som kan skickas inom 30 sekunder"
+ auto_silence_from_flags: "Chattmeddelande flaggat med tillräckligt hög poäng för att tysta användaren."
+ channel_cannot_be_archived: "Kanalen kan inte arkiveras just nu, den måste vara antingen stängd eller öppen för arkivering."
+ duplicate_message: "Du skrev också ett identiskt meddelande nyligen."
+ delete_channel_failed: "Det gick inte att ta bort kanalen, försök igen."
+ minimum_length_not_met: "Meddelandet är för kort, måste ha minst %{minimum} tecken."
+ max_reactions_limit_reached: "Nya reaktioner är inte tillåtna för detta meddelande."
+ message_move_invalid_channel: "Käll- och destinationskanalen måste vara offentliga kanaler."
+ message_move_no_messages_found: "Inga meddelanden hittades med de angivna meddelande-ID:n."
+ cant_update_direct_message_channel: "Egenskaper för direktmeddelandekanal såsom namn och beskrivning kan inte uppdateras."
+ not_accepting_dms: "Tyvärr tar %{username} inte emot meddelanden för tillfället."
+ actor_ignoring_target_user: "Du ignorerar %{username}, så du kan inte skicka meddelanden till dem."
+ actor_muting_target_user: "Du har tystat %{username}, så du kan inte skicka meddelanden till dem."
+ actor_disallowed_dms: "Du har valt att hindra användare från att skicka dig privata och direkta meddelanden, så du kan inte skapa nya direkta meddelanden."
+ actor_preventing_target_user_from_dm: "Du har valt att hindra %{username} från att skicka privata och direkta meddelanden, så du kan inte skapa nya direktmeddelanden till dem."
+ user_cannot_send_direct_messages: "Tyvärr kan du inte skicka direktmeddelanden."
+ reviewables:
+ message_already_handled: "Tack, men vi har redan granskat det här meddelandet och beslutat att det inte behöver flaggas igen."
+ actions:
+ agree:
+ title: "Godkänn..."
+ agree_and_keep_message:
+ title: "Behåll meddelande"
+ description: "Håll med flaggning men behåll meddelandet oförändrat."
+ agree_and_keep_deleted:
+ title: "Behåll meddelandet raderat"
+ description: "Håll med flaggning och behåll meddelandet raderat."
+ agree_and_suspend:
+ title: "Stäng av användaren"
+ description: "Godkänn flaggning och stäng av användaren."
+ agree_and_silence:
+ title: "Tysta användaren"
+ description: "Godkänn flaggning och tysta användaren."
+ agree_and_restore:
+ title: "Återställ meddelande"
+ description: "Återställ meddelandet så att användarna kan se det."
+ agree_and_delete:
+ title: "Radera meddelande"
+ description: "Ta bort meddelandet så att användarna inte kan se det."
+ delete_and_agree:
+ title: "Radera meddelande"
+ disagree_and_restore:
+ title: "Håll inte med och återställ meddelande"
+ description: "Återställ meddelandet så att alla användare kan se det."
+ disagree:
+ title: "Håll inte med"
+ ignore:
+ title: "Ignorera"
+ direct_messages:
+ transcript_title: "Avskrift av tidigare meddelanden i %{channel_name}"
+ transcript_body: "För att ge dig mer sammanhang inkluderade vi en avskrift av de tidigare meddelandena i det här samtalet (upp till tio):\n\n%{transcript}"
+ channel:
+ statuses:
+ read_only: "Endast läsning"
+ archived: "Arkiverad"
+ closed: "Stängd"
+ open: "Öppna"
+ archive:
+ first_post_raw: "Detta ämne är ett arkiv av chatt kanalen [%{channel_name}](%{channel_url})."
+ messages_moved:
+ one: "@%{acting_username} flyttade ett meddelande till kanalen [%{channel_name}](%{first_moved_message_url})."
+ other: "@%{acting_username} flyttade %{count} meddelanden till kanalen [%{channel_name}](%{first_moved_message_url})."
+ dm_title:
+ single_user: "%{user}"
+ multi_user: "%{users}"
+ multi_user_truncated: "%{users} och %{leftover} andra"
+ bookmarkable:
+ notification_title: "meddelande i %{channel_name}"
+ personal_chat: "personlig chatt"
+ onebox:
+ inline_to_message: "Meddelande #%{message_id} av %{username} – #%{chat_channel}"
+ inline_to_channel: "Chatt #%{chat_channel}"
+ inline_to_topic_channel: "Chatt för ämne %{topic_title}"
+ x_members:
+ one: "%{count} medlem"
+ other: "%{count} medlemmar"
+ and_x_others:
+ one: "och %{count} annan"
+ other: "och %{count} andra"
+ discourse_push_notifications:
+ popup:
+ chat_mention:
+ direct: '%{username} nämnde dig i "%{channel}"'
+ other_type: '%{username} nämnde %{identifier} i "%{channel}"'
+ direct_message_chat_mention:
+ direct: "%{username} nämnde dig i en personlig chatt"
+ other_type: "%{username} nämnde %{identifier} i personlig chatt"
+ new_chat_message: '%{username} skickade ett meddelande i "%{channel}"'
+ new_direct_chat_message: "%{username} skickade ett meddelande i personlig chatt"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ title: Skicka chattmeddelande
+ reviewable_score_types:
+ needs_review:
+ title: "Behöver granskning"
+ notify_user:
+ chat_pm_title: 'Ditt chattmeddelande i "%{channel_name}"'
+ chat_pm_body: "%{link}\n\n%{message}"
+ notify_moderators:
+ chat_pm_title: 'Ett chattmeddelande i "%{channel_name}" kräver personalens uppmärksamhet'
+ chat_pm_body: "%{link}\n\n%{message}"
+ reviewables:
+ reasons:
+ chat_message_queued_by_staff: "En anställd anser att detta chattmeddelande behöver granskas."
+ user_notifications:
+ chat_summary:
+ deleted_user: "Raderad användare"
+ description:
+ one: "Du har ett nytt chattmeddelande"
+ other: "Du har nya chattmeddelanden"
+ from: "%{site_name}"
+ subject:
+ direct_message:
+ one: "[%{email_prefix}] Nytt meddelande från %{message_title}"
+ other: "[%{email_prefix}] Nya meddelanden från %{message_title} och %{others}"
+ chat_channel:
+ one: "[%{email_prefix}] Nytt meddelande i %{message_title}"
+ other: "[%{email_prefix}] Nya meddelanden i %{message_title} och %{others}"
+ other_direct_message: "från %{message_title}"
+ others: "%{count} andra"
+ unsubscribe: "Den här chattsammanfattningen skickas från %{site_link} när du är borta. Ändra din %{email_preferences_link} eller %{unsubscribe_link} för att avsluta prenumerationen."
+ unsubscribe_no_link: "Den här chattsammanfattningen skickas från %{site_link} när du är borta. Ändra din %{email_preferences_link}."
+ view_messages:
+ one: "Visa meddelande"
+ other: "Visa %{count} meddelanden"
+ view_more:
+ one: "Visa %{count} mer meddelande"
+ other: "Visa %{count} fler meddelanden"
+ your_chat_settings: "preferens för frekvens av chattmeddelanden"
+ unsubscribe:
+ chat_summary:
+ select_title: "Ställ in e-postfrekvensen för chattsammanfattningar till:"
+ never: Aldrig
+ when_away: Endast när du är borta
+ category:
+ cannot_delete:
+ has_chat_channels: "Du kan inte ta bort den här kategorin eftersom den har chattkanaler."
diff --git a/plugins/chat/config/locales/server.sw.yml b/plugins/chat/config/locales/server.sw.yml
new file mode 100644
index 0000000000..24aa973047
--- /dev/null
+++ b/plugins/chat/config/locales/server.sw.yml
@@ -0,0 +1,35 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+sw:
+ chat:
+ deleted_chat_username: imefutwa
+ errors:
+ not_accepting_dms: "Samahani, %{username}haikubali jumbe kwa sasa"
+ reviewables:
+ actions:
+ agree_and_suspend:
+ title: "Simamisha Mtumiaji"
+ description: "Kubaliana na bendera na simamisha mtumiaji."
+ agree_and_silence:
+ title: "Nyamazisha Mtumiaji"
+ description: "Kubaliana na bendera na nyamazisha mtumiaji."
+ disagree:
+ title: "Kataa"
+ ignore:
+ title: "Puuzia"
+ channel:
+ statuses:
+ closed: "Imefungwa"
+ open: "Fungua"
+ reviewable_score_types:
+ notify_user:
+ chat_pm_body: "%{link}\n\n%{message}"
+ notify_moderators:
+ chat_pm_body: "%{link}\n\n%{message}"
+ unsubscribe:
+ chat_summary:
+ never: Kamwe
diff --git a/plugins/chat/config/locales/server.te.yml b/plugins/chat/config/locales/server.te.yml
new file mode 100644
index 0000000000..7fd9140d31
--- /dev/null
+++ b/plugins/chat/config/locales/server.te.yml
@@ -0,0 +1,20 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+te:
+ chat:
+ deleted_chat_username: తొలగించారు
+ reviewables:
+ actions:
+ agree_and_suspend:
+ title: "సభ్యుడిని సస్పెండు చేయి"
+ disagree:
+ title: "ఒప్పుకోకు"
+ reviewable_score_types:
+ notify_user:
+ chat_pm_body: "%{link}\n\n%{message}"
+ notify_moderators:
+ chat_pm_body: "%{link}\n\n%{message}"
diff --git a/plugins/chat/config/locales/server.th.yml b/plugins/chat/config/locales/server.th.yml
new file mode 100644
index 0000000000..9fd7f0cdb1
--- /dev/null
+++ b/plugins/chat/config/locales/server.th.yml
@@ -0,0 +1,26 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+th:
+ chat:
+ deleted_chat_username: ลบ
+ reviewables:
+ actions:
+ disagree:
+ title: "ไม่เห็นด้วย"
+ ignore:
+ title: "ไม่สนใจ"
+ channel:
+ statuses:
+ closed: "ปิด"
+ open: "เปิด"
+ discourse_push_notifications:
+ popup:
+ chat_mention:
+ direct: '%{username} พูดถึงคุณใน "%{channel}"'
+ unsubscribe:
+ chat_summary:
+ never: ไม่เคย
diff --git a/plugins/chat/config/locales/server.tr_TR.yml b/plugins/chat/config/locales/server.tr_TR.yml
new file mode 100644
index 0000000000..7acbd5d578
--- /dev/null
+++ b/plugins/chat/config/locales/server.tr_TR.yml
@@ -0,0 +1,46 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+tr_TR:
+ chat:
+ deleted_chat_username: silindi
+ errors:
+ not_accepting_dms: "Üzgünüz, %{username} şu anda ileti kabul etmiyor."
+ reviewables:
+ actions:
+ agree:
+ title: "Kabul..."
+ agree_and_suspend:
+ title: "Kullanıcıyı Askıya Al"
+ description: "Bayrakla işaretle ve kullanıcıyı askıya al."
+ agree_and_silence:
+ title: "Kullanıcıyı Sessize Al"
+ description: "Bayrakla işaretle ve kullanıcıyı sessize al."
+ disagree:
+ title: "Onaylama"
+ ignore:
+ title: "Yok say"
+ channel:
+ statuses:
+ closed: "Kapanmış"
+ open: "Başlat"
+ dm_title:
+ multi_user_truncated: "%{users} ve diğer %{leftover}"
+ discourse_push_notifications:
+ popup:
+ chat_mention:
+ direct: '%{username} sizden bahsetti "%{channel}"'
+ reviewable_score_types:
+ notify_user:
+ chat_pm_body: "%{link}\n\n%{message}"
+ notify_moderators:
+ chat_pm_body: "%{link}\n\n%{message}"
+ user_notifications:
+ chat_summary:
+ from: "%{site_name}"
+ unsubscribe:
+ chat_summary:
+ never: Asla
diff --git a/plugins/chat/config/locales/server.uk.yml b/plugins/chat/config/locales/server.uk.yml
new file mode 100644
index 0000000000..2fbe76c9e3
--- /dev/null
+++ b/plugins/chat/config/locales/server.uk.yml
@@ -0,0 +1,41 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+uk:
+ chat:
+ deleted_chat_username: видалено
+ errors:
+ not_accepting_dms: "На жаль, %{username} в даний момент не приймає повідомлення."
+ reviewables:
+ actions:
+ agree:
+ title: "Погодитися…"
+ agree_and_suspend:
+ title: "Призупинити користувача"
+ description: "Погодитися з прапором та заморозити користувача."
+ agree_and_silence:
+ title: "Заблокувати користувача"
+ description: "Погодитися з прапором та відключити користувача."
+ disagree:
+ title: "Відмовити"
+ ignore:
+ title: "Ігнорувати"
+ channel:
+ statuses:
+ closed: "Закриті"
+ open: "Відкрити"
+ discourse_push_notifications:
+ popup:
+ chat_mention:
+ direct: '%{username} згадав вас у "%{channel}"'
+ reviewable_score_types:
+ notify_user:
+ chat_pm_body: "%{link}\n\n%{message}\n"
+ notify_moderators:
+ chat_pm_body: "%{link}\n\n%{message}\n"
+ unsubscribe:
+ chat_summary:
+ never: Ніколи
diff --git a/plugins/chat/config/locales/server.ur.yml b/plugins/chat/config/locales/server.ur.yml
new file mode 100644
index 0000000000..5af33ad34b
--- /dev/null
+++ b/plugins/chat/config/locales/server.ur.yml
@@ -0,0 +1,37 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+ur:
+ chat:
+ deleted_chat_username: حذف کردہ
+ errors:
+ not_accepting_dms: "معذرت، %{username} اِس وقت پیغامات قبول نہیں کر رہا ہے۔"
+ reviewables:
+ actions:
+ agree:
+ title: "اتفاق کریں..."
+ agree_and_suspend:
+ title: "صارف معطل کریں"
+ description: "فلَیگ کے ساتھ اتفاق کریں اور صارف معطل کریں۔"
+ agree_and_silence:
+ title: "صارف خاموش کریں"
+ description: "فلَیگ کے ساتھ اتفاق کریں اور صارف خاموش کریں۔"
+ disagree:
+ title: "اختلاف کریں"
+ ignore:
+ title: "نظر انداز کریں"
+ channel:
+ statuses:
+ closed: "بند"
+ open: "کھولیں"
+ reviewable_score_types:
+ notify_user:
+ chat_pm_body: "%{link}\n\n%{message}"
+ notify_moderators:
+ chat_pm_body: "%{link}\n\n%{message}"
+ unsubscribe:
+ chat_summary:
+ never: کبھی نہیں
diff --git a/plugins/chat/config/locales/server.vi.yml b/plugins/chat/config/locales/server.vi.yml
new file mode 100644
index 0000000000..00d887158f
--- /dev/null
+++ b/plugins/chat/config/locales/server.vi.yml
@@ -0,0 +1,39 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+vi:
+ chat:
+ deleted_chat_username: đã bị xóa
+ errors:
+ not_accepting_dms: "Xin lỗi, %{username} hiện không chấp nhận tin nhắn."
+ reviewables:
+ actions:
+ agree_and_suspend:
+ title: "Tạm ngưng người dùng"
+ agree_and_silence:
+ title: "Người dùng im lặng"
+ disagree:
+ title: "Không đồng ý"
+ ignore:
+ title: "Bỏ qua"
+ channel:
+ statuses:
+ closed: "Đã "
+ open: "Mở"
+ dm_title:
+ multi_user_truncated: "%{users} và %{leftover} khác"
+ discourse_push_notifications:
+ popup:
+ chat_mention:
+ direct: '%{username} nhắc đến bạn trong "%{channel}"'
+ reviewable_score_types:
+ notify_user:
+ chat_pm_body: "%{link}\n\n%{message}"
+ notify_moderators:
+ chat_pm_body: "%{link}\n\n%{message}"
+ unsubscribe:
+ chat_summary:
+ never: Không bao giờ
diff --git a/plugins/chat/config/locales/server.zh_CN.yml b/plugins/chat/config/locales/server.zh_CN.yml
new file mode 100644
index 0000000000..c4a508baed
--- /dev/null
+++ b/plugins/chat/config/locales/server.zh_CN.yml
@@ -0,0 +1,176 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+zh_CN:
+ site_settings:
+ chat_allowed_groups: "这些群组中的用户可以聊天。请注意,管理人员始终可以访问聊天。"
+ chat_channel_retention_days: "常规频道中的聊天消息将保留此天数。设置为 0 将永久保留消息。"
+ chat_dm_retention_days: "个人聊天频道中的聊天消息将保留此天数。设置为 0 将永久保留消息。"
+ chat_auto_silence_duration: "用户超过聊天消息创建速率限制时将被禁言的分钟数。设置为 0 将禁用自动禁言。"
+ chat_allowed_messages_for_trust_level_0: "信任级别 0 用户在 30 秒内可以发送的消息数。设置为 0 将禁用限制。"
+ chat_allowed_messages_for_other_trust_levels: "信任级别 1-4 的用户在 30 秒内可以发送的消息数。设置为 0 将禁用限制。"
+ chat_silence_user_sensitivity: "聊天中被举报的用户被自动禁言的可能性。"
+ chat_auto_silence_from_flags_duration: "用户由于被举报的聊天消息而被自动禁言时将被禁言的分钟数。"
+ chat_default_channel_id: "当用户在其他频道中没有未读消息或提及时,将默认打开的聊天频道。"
+ chat_duplicate_message_sensitivity: "同一发件人的重复邮件在短时间内被屏蔽的可能性。0 到 1.0 之间的十进制数,1.0 是最高设置(在更短的时间内更频繁地屏蔽消息)。设置为 0 将允许重复消息。"
+ chat_minimum_message_length: "聊天消息的最少字符数。"
+ chat_allow_uploads: "允许在公共聊天频道和直接消息频道中上传文件。"
+ chat_archive_destination_topic_status: "频道归档完成后目标话题应处于的状态。这仅适用于目标话题是新话题而不是现有话题的情况。"
+ default_emoji_reactions: "聊天消息的默认表情符号反应。最多可添加 5 个表情符号进行快速反应。"
+ direct_message_enabled_groups: "允许这些群组中的用户创建用户间的个人聊天。请注意:管理人员始终可以创建个人聊天,用户将能够回复有权创建个人聊天的用户发起的个人聊天。"
+ chat_message_flag_allowed_groups: "这些群组中的用户可以举报聊天消息。"
+ errors:
+ chat_default_channel: "默认聊天频道必须是公共频道。"
+ direct_message_enabled_groups_invalid: "您必须为此设置至少指定一个群组。如果您不希望管理人员以外的任何人发送直接消息,请选择管理人员群组。"
+ chat_upload_not_allowed_secure_uploads: "当启用安全上传站点设置时,不允许在聊天中上传文件。"
+ system_messages:
+ chat_channel_archive_complete:
+ title: "聊天频道归档完成"
+ subject_template: "聊天频道归档成功完成"
+ text_body_template: |
+ 聊天频道**\#%{channel_name}**归档已成功完成。消息已被复制到话题[%{topic_title}](%{topic_url})中。
+ chat_channel_archive_failed:
+ title: "聊天频道归档失败"
+ subject_template: "聊天频道归档失败"
+ text_body_template: |
+ 聊天频道**#%{channel_name}**归档失败。%{messages_archived} 条消息已被归档。部分归档的消息已被复制到话题[%{topic_title}](%{topic_url})。请访问 %{channel_url} 下的频道以重试。
+ chat:
+ deleted_chat_username: 已删除
+ errors:
+ channel_exists_for_category: "此类别和名称的频道已经存在"
+ channel_new_message_disallowed: "频道的状态为%{status},无法发送新消息"
+ channel_modify_message_disallowed: "频道的状态为%{status},无法编辑或删除消息"
+ user_cannot_send_message: "您目前无法发送消息。"
+ rate_limit_exceeded: "超过了 30 秒内可发送的聊天消息的上限"
+ auto_silence_from_flags: "聊天消息被举报的分数高到足以将用户禁言。"
+ channel_cannot_be_archived: "目前无法归档该频道,必须将其关闭或打开才能归档。"
+ duplicate_message: "您在短时间内发布了一条相同的消息。"
+ delete_channel_failed: "删除频道失败,请重试。"
+ minimum_length_not_met: "消息太短,必须至少有 %{minimum} 个字符。"
+ max_reactions_limit_reached: "此消息不允许有新的回应。"
+ message_move_invalid_channel: "源频道和目标频道必须是公共频道。"
+ message_move_no_messages_found: "找不到带有提供的消息 ID 的消息。"
+ cant_update_direct_message_channel: "无法更新名称和描述等直接消息频道属性。"
+ not_accepting_dms: "抱歉,%{username} 目前不接受消息。"
+ actor_ignoring_target_user: "您正在忽略 %{username},因此您无法向他们发送消息。"
+ actor_muting_target_user: "您正在将 %{username} 设为免打扰,因此您无法向他们发送消息。"
+ actor_disallowed_dms: "您已选择阻止用户向您发送私人和直接消息,因此您无法创建新的直接消息。"
+ actor_preventing_target_user_from_dm: "您已选择阻止 %{username} 向您发送私人和直接消息,因此您无法创建给他们的新直接消息。"
+ user_cannot_send_direct_messages: "抱歉,您无法发送直接消息。"
+ reviewables:
+ message_already_handled: "谢谢,但我们已经审核此消息,并确定它不需要被再次举报。"
+ actions:
+ agree:
+ title: "同意…"
+ agree_and_keep_message:
+ title: "保留消息"
+ description: "同意举报并保持消息不变。"
+ agree_and_keep_deleted:
+ title: "保持消息删除状态"
+ description: "同意举报并保持消息删除状态。"
+ agree_and_suspend:
+ title: "封禁用户"
+ description: "同意举报并封禁用户。"
+ agree_and_silence:
+ title: "将用户禁言"
+ description: "同意举报并将用户禁言。"
+ agree_and_restore:
+ title: "恢复消息"
+ description: "恢复消息,以便用户可以看到。"
+ agree_and_delete:
+ title: "删除消息"
+ description: "删除消息,使用户看不到。"
+ delete_and_agree:
+ title: "删除消息"
+ disagree_and_restore:
+ title: "不同意并恢复消息"
+ description: "恢复消息,以便所有用户都可以看到。"
+ disagree:
+ title: "不同意"
+ ignore:
+ title: "忽略"
+ direct_messages:
+ transcript_title: "%{channel_name}中以前消息的副本"
+ transcript_body: "为了向您提供更多上下文,我们在此对话中包含了以前消息的副本(最多十条):\n\n%{transcript}"
+ channel:
+ statuses:
+ read_only: "只读"
+ archived: "已归档"
+ closed: "已关闭"
+ open: "开放"
+ archive:
+ first_post_raw: "此话题是[%{channel_name}](%{channel_url})聊天频道的归档。"
+ messages_moved:
+ other: "@%{acting_username}将 %{count} 条消息移至[%{channel_name}](%{first_moved_message_url})频道。"
+ dm_title:
+ single_user: "%{user}"
+ multi_user: "%{users}"
+ multi_user_truncated: "%{users} 和其他 %{leftover} 人"
+ bookmarkable:
+ notification_title: "%{channel_name}中的消息"
+ personal_chat: "个人聊天"
+ onebox:
+ inline_to_message: "消息 #%{message_id},来自%{username} – #%{chat_channel}"
+ inline_to_channel: "聊天 #%{chat_channel}"
+ inline_to_topic_channel: "话题%{topic_title}的聊天"
+ x_members:
+ other: "%{count} 个成员"
+ and_x_others:
+ other: "和其他 %{count} 人"
+ discourse_push_notifications:
+ popup:
+ chat_mention:
+ direct: '%{username} 在“%{channel}”中提及您'
+ other_type: '%{username} 在“%{channel}”中提及“%{identifier}”'
+ direct_message_chat_mention:
+ direct: "%{username} 在个人聊天中提及您"
+ other_type: "%{username} 在个人聊天中提及“%{identifier}”"
+ new_chat_message: '%{username} 在“%{channel}”中发送了一条消息'
+ new_direct_chat_message: "%{username} 在个人聊天中发送了一条消息"
+ discourse_automation:
+ scriptables:
+ send_chat_message:
+ title: 发送聊天消息
+ reviewable_score_types:
+ needs_review:
+ title: "需要审核"
+ notify_user:
+ chat_pm_title: '您在“%{channel_name}”中的聊天消息'
+ chat_pm_body: "%{link}\n\n%{message}"
+ notify_moderators:
+ chat_pm_title: '“%{channel_name}”中的一条聊天消息需要管理人员注意'
+ chat_pm_body: "%{link}\n\n%{message}"
+ reviewables:
+ reasons:
+ chat_message_queued_by_staff: "一位管理人员认为此聊天消息需要审核。"
+ user_notifications:
+ chat_summary:
+ deleted_user: "已被删除的用户"
+ description:
+ other: "您有新的聊天消息"
+ from: "%{site_name}"
+ subject:
+ direct_message:
+ other: "[%{email_prefix}] 来自%{message_title}和%{others}的新消息"
+ chat_channel:
+ other: "[%{email_prefix}] %{message_title}和%{others}中的新消息"
+ other_direct_message: "来自%{message_title}"
+ others: "其他 %{count} 人"
+ unsubscribe: "此聊天摘要在您离开时从%{site_link}发送。更改您的%{email_preferences_link},或者%{unsubscribe_link}以退订。"
+ unsubscribe_no_link: "此聊天摘要在您离开时从%{site_link}发送。更改您的%{email_preferences_link}。"
+ view_messages:
+ other: "查看 %{count} 条消息"
+ view_more:
+ other: "查看其他 %{count} 条消息"
+ your_chat_settings: "聊天电子邮件频率偏好设置"
+ unsubscribe:
+ chat_summary:
+ select_title: "将聊天摘要电子邮件频率设置为:"
+ never: 永不
+ when_away: 仅在离开时
+ category:
+ cannot_delete:
+ has_chat_channels: "无法删除此类别,因为它有关联的聊天频道。"
diff --git a/plugins/chat/config/locales/server.zh_TW.yml b/plugins/chat/config/locales/server.zh_TW.yml
new file mode 100644
index 0000000000..08d87ec741
--- /dev/null
+++ b/plugins/chat/config/locales/server.zh_TW.yml
@@ -0,0 +1,37 @@
+# WARNING: Never edit this file.
+# It will be overwritten when translations are pulled from Crowdin.
+#
+# To work with us on translations, join this project:
+# https://translate.discourse.org/
+
+zh_TW:
+ chat:
+ deleted_chat_username: 已刪除
+ errors:
+ not_accepting_dms: "對不起,%{username} 目前不接收訊息。"
+ reviewables:
+ actions:
+ agree:
+ title: "同意..."
+ agree_and_suspend:
+ title: "將使用者停權"
+ description: "同意檢舉並停權使用者"
+ agree_and_silence:
+ title: "將使用者禁言"
+ description: "同意檢舉並禁止使用者發文"
+ disagree:
+ title: "不同意"
+ ignore:
+ title: "忽略"
+ channel:
+ statuses:
+ closed: "不公開"
+ open: "開啟"
+ reviewable_score_types:
+ notify_user:
+ chat_pm_body: "%{link}\n\n%{message}"
+ notify_moderators:
+ chat_pm_body: "%{link}\n\n%{message}"
+ unsubscribe:
+ chat_summary:
+ never: 永不
diff --git a/plugins/chat/config/settings.yml b/plugins/chat/config/settings.yml
new file mode 100644
index 0000000000..0ded290e0e
--- /dev/null
+++ b/plugins/chat/config/settings.yml
@@ -0,0 +1,95 @@
+chat:
+ chat_enabled:
+ default: false
+ client: true
+ chat_allowed_groups:
+ client: true
+ type: group_list
+ list_type: compact
+ default: "3" # 3 is staff group id
+ allow_any: false
+ refresh: true
+ needs_chat_seeded:
+ default: true
+ hidden: true
+ chat_debug_webhook_payloads:
+ default: false
+ hidden: true
+ chat_channel_retention_days:
+ default: 90
+ client: true
+ max: 3652 # 10 years
+ min: 0
+ chat_dm_retention_days:
+ default: 0
+ client: true
+ max: 3652 # 10 years
+ min: 0
+ chat_auto_silence_duration:
+ default: 30
+ min: 0
+ chat_allowed_messages_for_trust_level_0:
+ default: 20
+ min: 0
+ chat_allowed_messages_for_other_trust_levels:
+ default: 40
+ min: 0
+ chat_silence_user_sensitivity:
+ type: enum
+ enum: "ReviewableSensitivitySetting"
+ default: 6
+ chat_auto_silence_from_flags_duration:
+ default: 60
+ min: 0
+ chat_allow_archiving_channels:
+ default: false
+ hidden: true
+ client: true
+ chat_archive_destination_topic_status:
+ type: enum
+ default: archived
+ choices:
+ - archived
+ - open
+ - closed
+ chat_default_channel_id:
+ default: ""
+ client: true
+ validator: "ChatDefaultChannelValidator"
+ chat_duplicate_message_sensitivity:
+ type: float
+ default: 0.5
+ min: 0
+ max: 1
+ default_emoji_reactions:
+ type: emoji_list
+ default: +1|heart|tada
+ client: true
+ chat_minimum_message_length:
+ type: integer
+ default: 1
+ min: 1
+ max: 50
+ client: true
+ chat_allow_uploads:
+ default: true
+ client: true
+ validator: "ChatAllowUploadsValidator"
+ max_chat_auto_joined_users:
+ min: 0
+ default: 10000
+ hidden: true
+ client: true
+ direct_message_enabled_groups:
+ default: "11" # auto group trust_level_1
+ type: group_list
+ client: true
+ allow_any: false
+ refresh: true
+ validator: "DirectMessageEnabledGroupsValidator"
+ chat_message_flag_allowed_groups:
+ default: "11" # auto group trust_level_1
+ type: group_list
+ client: true
+ allow_any: false
+ refresh: true
diff --git a/plugins/chat/db/fixtures/600_chat_channels.rb b/plugins/chat/db/fixtures/600_chat_channels.rb
new file mode 100644
index 0000000000..972398ba7f
--- /dev/null
+++ b/plugins/chat/db/fixtures/600_chat_channels.rb
@@ -0,0 +1,3 @@
+# frozen_string_literal: true
+
+ChatSeeder.new.execute if !Rails.env.test?
diff --git a/plugins/chat/db/migrate/20210225230057_create_chat_tables.rb b/plugins/chat/db/migrate/20210225230057_create_chat_tables.rb
new file mode 100644
index 0000000000..21844f7ea6
--- /dev/null
+++ b/plugins/chat/db/migrate/20210225230057_create_chat_tables.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+class CreateChatTables < ActiveRecord::Migration[6.0]
+ def change
+ create_table :topic_chats do |t|
+ t.integer :topic_id, null: false, index: true, unique: true
+ t.datetime :deleted_at
+ t.integer :deleted_by_id
+
+ t.integer :featured_in_category_id
+ t.integer :delete_after_seconds, default: nil
+ end
+
+ create_table :topic_chat_messages do |t|
+ t.integer :topic_id, null: false
+ t.integer :post_id, null: false, index: true
+ t.integer :user_id, null: true
+ t.timestamps
+ t.datetime :deleted_at
+ t.integer :deleted_by_id
+ t.integer :in_reply_to_id, null: true
+ t.text :message
+ end
+
+ add_index :topic_chat_messages, %i[topic_id created_at]
+ end
+end
diff --git a/plugins/chat/db/migrate/20210403025854_add_action_code_to_topic_chat_message.rb b/plugins/chat/db/migrate/20210403025854_add_action_code_to_topic_chat_message.rb
new file mode 100644
index 0000000000..522ad28f16
--- /dev/null
+++ b/plugins/chat/db/migrate/20210403025854_add_action_code_to_topic_chat_message.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddActionCodeToTopicChatMessage < ActiveRecord::Migration[6.0]
+ def change
+ add_column :topic_chat_messages, :action_code, :string, null: true
+ end
+end
diff --git a/plugins/chat/db/migrate/20210706214013_rename_topic_chats_to_chat_channels.rb b/plugins/chat/db/migrate/20210706214013_rename_topic_chats_to_chat_channels.rb
new file mode 100644
index 0000000000..134417d385
--- /dev/null
+++ b/plugins/chat/db/migrate/20210706214013_rename_topic_chats_to_chat_channels.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+class RenameTopicChatsToChatChannels < ActiveRecord::Migration[6.1]
+ def up
+ begin
+ Migration::SafeMigrate.disable!
+
+ # Trash all existing chat info
+ DB.exec("DELETE FROM topic_chats")
+ DB.exec("DELETE FROM topic_chat_messages")
+
+ # topic_chat table changes
+ rename_table :topic_chats, :chat_channels
+ rename_column :chat_channels, :topic_id, :chatable_id
+ change_column :chat_channels, :chatable_id, :integer, unique: false
+ add_column :chat_channels, :chatable_type, :string
+ change_column_null :chat_channels, :chatable_type, false
+ add_index :chat_channels, %i[chatable_id chatable_type]
+
+ # topic_chat_messages table changes
+ rename_table :topic_chat_messages, :chat_messages
+ rename_column :chat_messages, :topic_id, :chat_channel_id
+ change_column_null :chat_messages, :post_id, true # Don't require post_id
+ ensure
+ Migration::SafeMigrate.enable!
+ end
+ end
+
+ def down
+ raise ActiveRecord::IrreversibleMigration
+ end
+end
diff --git a/plugins/chat/db/migrate/20210729134042_create_chat_message_revisions.rb b/plugins/chat/db/migrate/20210729134042_create_chat_message_revisions.rb
new file mode 100644
index 0000000000..423ccffd41
--- /dev/null
+++ b/plugins/chat/db/migrate/20210729134042_create_chat_message_revisions.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class CreateChatMessageRevisions < ActiveRecord::Migration[6.1]
+ def change
+ create_table :chat_message_revisions do |t|
+ t.integer :chat_message_id
+ t.text :old_message, null: false
+ t.text :new_message, null: false
+ t.timestamps
+ end
+
+ add_index :chat_message_revisions, [:chat_message_id]
+ end
+end
diff --git a/plugins/chat/db/migrate/20210730134847_create_user_chat_channel_last_read.rb b/plugins/chat/db/migrate/20210730134847_create_user_chat_channel_last_read.rb
new file mode 100644
index 0000000000..a0b7304687
--- /dev/null
+++ b/plugins/chat/db/migrate/20210730134847_create_user_chat_channel_last_read.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class CreateUserChatChannelLastRead < ActiveRecord::Migration[6.1]
+ def change
+ create_table :user_chat_channel_last_reads do |t|
+ t.integer :chat_channel_id, null: false
+ t.integer :chat_message_id, null: true # Can be null if user hasn't opened the channel
+ t.integer :user_id, null: false
+ end
+
+ add_index :user_chat_channel_last_reads,
+ %i[chat_channel_id user_id],
+ unique: true,
+ name: "user_chat_channel_reads_index"
+ end
+end
diff --git a/plugins/chat/db/migrate/20210812145801_create_direct_message_tables.rb b/plugins/chat/db/migrate/20210812145801_create_direct_message_tables.rb
new file mode 100644
index 0000000000..3b231e0782
--- /dev/null
+++ b/plugins/chat/db/migrate/20210812145801_create_direct_message_tables.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class CreateDirectMessageTables < ActiveRecord::Migration[6.1]
+ def change
+ create_table :direct_message_channels do |t|
+ t.timestamps
+ end
+
+ create_table :direct_message_users do |t|
+ t.integer :direct_message_channel_id, null: false
+ t.integer :user_id, null: false
+ t.timestamps
+ end
+
+ add_index :direct_message_users,
+ %i[direct_message_channel_id user_id],
+ unique: true,
+ name: "direct_message_users_index"
+ end
+end
diff --git a/plugins/chat/db/migrate/20210813141741_add_timestamps_to_chat_channels.rb b/plugins/chat/db/migrate/20210813141741_add_timestamps_to_chat_channels.rb
new file mode 100644
index 0000000000..773a778c9f
--- /dev/null
+++ b/plugins/chat/db/migrate/20210813141741_add_timestamps_to_chat_channels.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+class AddTimestampsToChatChannels < ActiveRecord::Migration[6.1]
+ def change
+ add_column :chat_channels, :created_at, :timestamp
+ add_column :chat_channels, :updated_at, :timestamp
+
+ DB.exec("UPDATE chat_channels SET created_at = NOW() WHERE created_at IS NULL")
+ DB.exec("UPDATE chat_channels SET updated_at = NOW() WHERE updated_at IS NULL")
+
+ change_column_null :chat_channels, :created_at, false
+ change_column_null :chat_channels, :updated_at, false
+ end
+end
diff --git a/plugins/chat/db/migrate/20210819202912_create_incoming_chat_webhooks.rb b/plugins/chat/db/migrate/20210819202912_create_incoming_chat_webhooks.rb
new file mode 100644
index 0000000000..23cc115b74
--- /dev/null
+++ b/plugins/chat/db/migrate/20210819202912_create_incoming_chat_webhooks.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+class CreateIncomingChatWebhooks < ActiveRecord::Migration[6.1]
+ def change
+ create_table :incoming_chat_webhooks do |t|
+ t.string :name, null: false
+ t.string :key, null: false
+ t.integer :chat_channel_id, null: false
+ t.string :username
+ t.string :description
+ t.string :emoji
+
+ t.timestamps
+ end
+
+ add_index :incoming_chat_webhooks, %i[key chat_channel_id]
+ end
+end
diff --git a/plugins/chat/db/migrate/20210823160357_create_chat_webhook_events.rb b/plugins/chat/db/migrate/20210823160357_create_chat_webhook_events.rb
new file mode 100644
index 0000000000..4e95767754
--- /dev/null
+++ b/plugins/chat/db/migrate/20210823160357_create_chat_webhook_events.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+class CreateChatWebhookEvents < ActiveRecord::Migration[6.1]
+ def change
+ create_table :chat_webhook_events do |t|
+ t.integer :chat_message_id, null: false
+ t.integer :incoming_chat_webhook_id, null: false
+ t.timestamps
+ end
+
+ add_index :chat_webhook_events,
+ %i[chat_message_id incoming_chat_webhook_id],
+ unique: true,
+ name: "chat_webhook_events_index"
+ end
+end
diff --git a/plugins/chat/db/migrate/20210901130308_create_user_chat_channel_membership.rb b/plugins/chat/db/migrate/20210901130308_create_user_chat_channel_membership.rb
new file mode 100644
index 0000000000..81eea0a67c
--- /dev/null
+++ b/plugins/chat/db/migrate/20210901130308_create_user_chat_channel_membership.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+class CreateUserChatChannelMembership < ActiveRecord::Migration[6.1]
+ def change
+ create_table :user_chat_channel_memberships do |t|
+ t.integer :user_id, null: false
+ t.integer :chat_channel_id, null: false
+ t.integer :last_read_message_id
+ t.boolean :following, default: false, null: false # membership on/off switch
+ t.boolean :muted, default: false, null: false
+ t.integer :desktop_notification_level, default: 1, null: false
+ t.integer :mobile_notification_level, default: 1, null: false
+ t.timestamps
+ end
+
+ add_index :user_chat_channel_memberships,
+ %i[
+ user_id
+ chat_channel_id
+ desktop_notification_level
+ mobile_notification_level
+ following
+ ],
+ name: "user_chat_channel_memberships_index"
+
+ add_index :user_chat_channel_memberships,
+ %i[user_id chat_channel_id],
+ unique: true,
+ name: "user_chat_channel_unique_memberships"
+ end
+end
diff --git a/plugins/chat/db/migrate/20210930144333_add_chat_enabled_to_user_options.rb b/plugins/chat/db/migrate/20210930144333_add_chat_enabled_to_user_options.rb
new file mode 100644
index 0000000000..d30e690362
--- /dev/null
+++ b/plugins/chat/db/migrate/20210930144333_add_chat_enabled_to_user_options.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+class AddChatEnabledToUserOptions < ActiveRecord::Migration[6.1]
+ def change
+ add_column :user_options, :chat_enabled, :boolean, default: true, null: false
+ end
+end
diff --git a/plugins/chat/db/migrate/20211022151713_create_chat_message_post_connections.rb b/plugins/chat/db/migrate/20211022151713_create_chat_message_post_connections.rb
new file mode 100644
index 0000000000..dab116d3c0
--- /dev/null
+++ b/plugins/chat/db/migrate/20211022151713_create_chat_message_post_connections.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+class CreateChatMessagePostConnections < ActiveRecord::Migration[6.1]
+ def change
+ create_table :chat_message_post_connections do |t|
+ t.integer :post_id, null: false
+ t.integer :chat_message_id, null: false
+ t.timestamps
+ end
+
+ add_index :chat_message_post_connections,
+ %i[post_id chat_message_id],
+ unique: true,
+ name: "chat_message_post_connections_index"
+ end
+end
diff --git a/plugins/chat/db/migrate/20211029145508_add_chat_isolated_to_user_options.rb b/plugins/chat/db/migrate/20211029145508_add_chat_isolated_to_user_options.rb
new file mode 100644
index 0000000000..8844686c9a
--- /dev/null
+++ b/plugins/chat/db/migrate/20211029145508_add_chat_isolated_to_user_options.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddChatIsolatedToUserOptions < ActiveRecord::Migration[6.1]
+ def change
+ add_column :user_options, :chat_isolated, :boolean, null: true
+ end
+end
diff --git a/plugins/chat/db/migrate/20211104141254_add_only_chat_push_notifications_to_user_options.rb b/plugins/chat/db/migrate/20211104141254_add_only_chat_push_notifications_to_user_options.rb
new file mode 100644
index 0000000000..5de025e4ed
--- /dev/null
+++ b/plugins/chat/db/migrate/20211104141254_add_only_chat_push_notifications_to_user_options.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+class AddOnlyChatPushNotificationsToUserOptions < ActiveRecord::Migration[6.1]
+ def change
+ add_column :user_options, :only_chat_push_notifications, :boolean, null: true
+ end
+end
diff --git a/plugins/chat/db/migrate/20211119142000_add_cooked_to_chat_messages.rb b/plugins/chat/db/migrate/20211119142000_add_cooked_to_chat_messages.rb
new file mode 100644
index 0000000000..876de7cf2d
--- /dev/null
+++ b/plugins/chat/db/migrate/20211119142000_add_cooked_to_chat_messages.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+class AddCookedToChatMessages < ActiveRecord::Migration[6.1]
+ def change
+ add_column :chat_messages, :cooked, :text
+ add_column :chat_messages, :cooked_version, :integer
+ end
+end
diff --git a/plugins/chat/db/migrate/20211129171229_create_chat_uploads.rb b/plugins/chat/db/migrate/20211129171229_create_chat_uploads.rb
new file mode 100644
index 0000000000..7eb9d56683
--- /dev/null
+++ b/plugins/chat/db/migrate/20211129171229_create_chat_uploads.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class CreateChatUploads < ActiveRecord::Migration[6.1]
+ def change
+ create_table :chat_uploads do |t|
+ t.integer :chat_message_id, null: false
+ t.integer :upload_id, null: false
+ t.timestamps
+ end
+
+ add_index :chat_uploads, %i[chat_message_id upload_id], unique: true
+ end
+end
diff --git a/plugins/chat/db/migrate/20211201171813_create_chat_reactions.rb b/plugins/chat/db/migrate/20211201171813_create_chat_reactions.rb
new file mode 100644
index 0000000000..377849e509
--- /dev/null
+++ b/plugins/chat/db/migrate/20211201171813_create_chat_reactions.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+class CreateChatReactions < ActiveRecord::Migration[6.1]
+ def change
+ create_table :chat_message_reactions do |t|
+ t.integer :chat_message_id
+ t.integer :user_id
+ t.string :emoji
+ t.timestamps
+ end
+
+ add_index :chat_message_reactions,
+ %i[chat_message_id user_id emoji],
+ unique: true,
+ name: :chat_message_reactions_index
+ end
+end
diff --git a/plugins/chat/db/migrate/20211210191830_create_chat_mentions.rb b/plugins/chat/db/migrate/20211210191830_create_chat_mentions.rb
new file mode 100644
index 0000000000..26f42042d3
--- /dev/null
+++ b/plugins/chat/db/migrate/20211210191830_create_chat_mentions.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+class CreateChatMentions < ActiveRecord::Migration[6.1]
+ def change
+ create_table :chat_mentions do |t|
+ t.integer :chat_message_id, null: false
+ t.integer :user_id, null: false
+ t.integer :notification_id, null: false
+ t.timestamps
+ end
+
+ add_index :chat_mentions,
+ %i[chat_message_id user_id notification_id],
+ unique: true,
+ name: "chat_mentions_index"
+ end
+end
diff --git a/plugins/chat/db/migrate/20211213150607_add_chat_sound_to_user_options.rb b/plugins/chat/db/migrate/20211213150607_add_chat_sound_to_user_options.rb
new file mode 100644
index 0000000000..3eae79c91f
--- /dev/null
+++ b/plugins/chat/db/migrate/20211213150607_add_chat_sound_to_user_options.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+class AddChatSoundToUserOptions < ActiveRecord::Migration[6.1]
+ def change
+ add_column :user_options, :chat_sound, :string, null: true
+ end
+end
diff --git a/plugins/chat/db/migrate/20211217221026_add_name_to_chat_channel.rb b/plugins/chat/db/migrate/20211217221026_add_name_to_chat_channel.rb
new file mode 100644
index 0000000000..7a651cbca5
--- /dev/null
+++ b/plugins/chat/db/migrate/20211217221026_add_name_to_chat_channel.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+class AddNameToChatChannel < ActiveRecord::Migration[6.1]
+ def change
+ add_column :chat_channels, :name, :string, null: true
+ end
+end
diff --git a/plugins/chat/db/migrate/20211222153716_add_description_to_chat_channels.rb b/plugins/chat/db/migrate/20211222153716_add_description_to_chat_channels.rb
new file mode 100644
index 0000000000..6169fd8f8b
--- /dev/null
+++ b/plugins/chat/db/migrate/20211222153716_add_description_to_chat_channels.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+class AddDescriptionToChatChannels < ActiveRecord::Migration[6.1]
+ def change
+ add_column :chat_channels, :description, :text, null: true
+ end
+end
diff --git a/plugins/chat/db/migrate/20220119170535_add_chat_retention_fields_to_user_options.rb b/plugins/chat/db/migrate/20220119170535_add_chat_retention_fields_to_user_options.rb
new file mode 100644
index 0000000000..1b3c40d8cd
--- /dev/null
+++ b/plugins/chat/db/migrate/20220119170535_add_chat_retention_fields_to_user_options.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+class AddChatRetentionFieldsToUserOptions < ActiveRecord::Migration[6.1]
+ def change
+ add_column :user_options, :dismissed_channel_retention_reminder, :boolean, null: true
+ add_column :user_options, :dismissed_dm_retention_reminder, :boolean, null: true
+ end
+end
diff --git a/plugins/chat/db/migrate/20220203204002_create_chat_drafts_table.rb b/plugins/chat/db/migrate/20220203204002_create_chat_drafts_table.rb
new file mode 100644
index 0000000000..aa33c9d1fe
--- /dev/null
+++ b/plugins/chat/db/migrate/20220203204002_create_chat_drafts_table.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+class CreateChatDraftsTable < ActiveRecord::Migration[6.1]
+ def change
+ create_table :chat_drafts do |t|
+ t.integer :user_id, null: false
+ t.integer :chat_channel_id, null: false
+ t.text :data, null: false
+ t.timestamps
+ end
+ end
+end
diff --git a/plugins/chat/db/migrate/20220203204003_migrate_drafts_to_chat_drafts.rb b/plugins/chat/db/migrate/20220203204003_migrate_drafts_to_chat_drafts.rb
new file mode 100644
index 0000000000..8b624115ab
--- /dev/null
+++ b/plugins/chat/db/migrate/20220203204003_migrate_drafts_to_chat_drafts.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class MigrateDraftsToChatDrafts < ActiveRecord::Migration[6.1]
+ def up
+ execute <<~SQL
+ INSERT INTO chat_drafts(user_id, chat_channel_id, data, created_at, updated_at)
+ SELECT user_id, SUBSTRING(draft_key, LENGTH('chat_') + 1)::integer chat_channel_id, data, created_at, updated_at
+ FROM drafts
+ WHERE draft_key LIKE 'chat_%'
+ SQL
+
+ execute <<~SQL
+ DELETE FROM drafts
+ WHERE draft_key LIKE 'chat_%'
+ SQL
+ end
+
+ def down
+ raise ActiveRecord::IrreversibleMigration
+ end
+end
diff --git a/plugins/chat/db/migrate/20220218023859_add_status_to_chat_channel.rb b/plugins/chat/db/migrate/20220218023859_add_status_to_chat_channel.rb
new file mode 100644
index 0000000000..2ba06853db
--- /dev/null
+++ b/plugins/chat/db/migrate/20220218023859_add_status_to_chat_channel.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+#
+class AddStatusToChatChannel < ActiveRecord::Migration[6.1]
+ def change
+ add_column :chat_channels, :status, :integer, default: 0, null: false
+ add_index :chat_channels, :status
+ end
+end
diff --git a/plugins/chat/db/migrate/20220228051724_create_chat_channel_archive_table.rb b/plugins/chat/db/migrate/20220228051724_create_chat_channel_archive_table.rb
new file mode 100644
index 0000000000..077f467f0c
--- /dev/null
+++ b/plugins/chat/db/migrate/20220228051724_create_chat_channel_archive_table.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+#
+class CreateChatChannelArchiveTable < ActiveRecord::Migration[6.1]
+ def change
+ create_table :chat_channel_archives do |t|
+ t.integer :chat_channel_id, null: false
+ t.integer :archived_by_id, null: false
+ t.integer :destination_topic_id
+ t.string :destination_topic_title
+ t.integer :destination_category_id
+ t.column :destination_tags, :string, array: true
+ t.integer :total_messages, null: false
+ t.integer :archived_messages, default: 0, null: false
+ t.string :archive_error
+
+ t.timestamps
+ end
+
+ add_index :chat_channel_archives, :chat_channel_id
+ end
+end
diff --git a/plugins/chat/db/migrate/20220308165620_add_user_count_to_chat_channel.rb b/plugins/chat/db/migrate/20220308165620_add_user_count_to_chat_channel.rb
new file mode 100644
index 0000000000..efcc057cb2
--- /dev/null
+++ b/plugins/chat/db/migrate/20220308165620_add_user_count_to_chat_channel.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+class AddUserCountToChatChannel < ActiveRecord::Migration[6.1]
+ def change
+ add_column :chat_channels, :user_count, :integer, null: true, default: 0
+ change_column_null :chat_channels, :user_count, false
+ end
+end
diff --git a/plugins/chat/db/migrate/20220309174820_add_last_message_created_at_to_chat_channels.rb b/plugins/chat/db/migrate/20220309174820_add_last_message_created_at_to_chat_channels.rb
new file mode 100644
index 0000000000..5f07d4cf8c
--- /dev/null
+++ b/plugins/chat/db/migrate/20220309174820_add_last_message_created_at_to_chat_channels.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+class AddLastMessageCreatedAtToChatChannels < ActiveRecord::Migration[6.1]
+ def change
+ add_column :chat_channels, :last_message_sent_at, :datetime, default: -> { "CURRENT_TIMESTAMP" }
+ change_column_null :chat_channels, :last_message_sent_at, false
+ end
+end
diff --git a/plugins/chat/db/migrate/20220324062937_ignore_channel_wide_mention_to_user_options.rb b/plugins/chat/db/migrate/20220324062937_ignore_channel_wide_mention_to_user_options.rb
new file mode 100644
index 0000000000..d902c46281
--- /dev/null
+++ b/plugins/chat/db/migrate/20220324062937_ignore_channel_wide_mention_to_user_options.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+class IgnoreChannelWideMentionToUserOptions < ActiveRecord::Migration[6.1]
+ def change
+ add_column :user_options, :ignore_channel_wide_mention, :boolean, null: true
+ end
+end
diff --git a/plugins/chat/db/migrate/20220328142120_create_user_chat_message_statuses.rb b/plugins/chat/db/migrate/20220328142120_create_user_chat_message_statuses.rb
new file mode 100644
index 0000000000..a1012b44a8
--- /dev/null
+++ b/plugins/chat/db/migrate/20220328142120_create_user_chat_message_statuses.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class CreateUserChatMessageStatuses < ActiveRecord::Migration[6.1]
+ def change
+ create_table :chat_message_email_statuses do |t|
+ t.integer :chat_message_id, null: false
+ t.integer :user_id, null: false
+ t.integer :status, null: false, default: 0
+ t.integer :type, null: false
+ t.timestamps
+ end
+
+ add_index :chat_message_email_statuses,
+ %i[user_id chat_message_id],
+ name: "chat_message_email_status_user_message_index"
+ add_index :chat_message_email_statuses, :status
+
+ add_column :user_options, :chat_email_frequency, :integer, default: 1, null: false
+ add_column :user_options, :last_emailed_for_chat, :datetime, null: true
+ end
+end
diff --git a/plugins/chat/db/migrate/20220518140004_track_last_unread_mention_when_emailed.rb b/plugins/chat/db/migrate/20220518140004_track_last_unread_mention_when_emailed.rb
new file mode 100644
index 0000000000..bf914e8da7
--- /dev/null
+++ b/plugins/chat/db/migrate/20220518140004_track_last_unread_mention_when_emailed.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class TrackLastUnreadMentionWhenEmailed < ActiveRecord::Migration[7.0]
+ def change
+ add_column :user_chat_channel_memberships, :last_unread_mention_when_emailed_id, :integer
+ end
+end
diff --git a/plugins/chat/db/migrate/20220629190633_auto_join_users_to_channels.rb b/plugins/chat/db/migrate/20220629190633_auto_join_users_to_channels.rb
new file mode 100644
index 0000000000..b1117a9c05
--- /dev/null
+++ b/plugins/chat/db/migrate/20220629190633_auto_join_users_to_channels.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AutoJoinUsersToChannels < ActiveRecord::Migration[7.0]
+ def change
+ add_column :chat_channels, :auto_join_users, :boolean, null: false, default: false
+ end
+end
diff --git a/plugins/chat/db/migrate/20220706114835_add_join_mode_to_channel_memberships.rb b/plugins/chat/db/migrate/20220706114835_add_join_mode_to_channel_memberships.rb
new file mode 100644
index 0000000000..2ab6ea1644
--- /dev/null
+++ b/plugins/chat/db/migrate/20220706114835_add_join_mode_to_channel_memberships.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddJoinModeToChannelMemberships < ActiveRecord::Migration[7.0]
+ def change
+ add_column :user_chat_channel_memberships, :join_mode, :integer, null: false, default: 0
+ end
+end
diff --git a/plugins/chat/db/migrate/20220729032237_add_index_to_chat_message_created_at.rb b/plugins/chat/db/migrate/20220729032237_add_index_to_chat_message_created_at.rb
new file mode 100644
index 0000000000..16a2197be4
--- /dev/null
+++ b/plugins/chat/db/migrate/20220729032237_add_index_to_chat_message_created_at.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class AddIndexToChatMessageCreatedAt < ActiveRecord::Migration[7.0]
+ disable_ddl_transaction!
+
+ def change
+ execute <<~SQL
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS
+ idx_chat_messages_by_created_at_not_deleted
+ ON chat_messages (created_at)
+ WHERE deleted_at IS NULL
+ SQL
+ end
+
+ def down
+ execute <<~SQL
+ DROP INDEX IF EXISTS idx_chat_messages_by_created_at_not_deleted
+ SQL
+ end
+end
diff --git a/plugins/chat/db/migrate/20220802014549_disable_chat_uploads_if_secure_media_enabled.rb b/plugins/chat/db/migrate/20220802014549_disable_chat_uploads_if_secure_media_enabled.rb
new file mode 100644
index 0000000000..49a8833cd8
--- /dev/null
+++ b/plugins/chat/db/migrate/20220802014549_disable_chat_uploads_if_secure_media_enabled.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+class DisableChatUploadsIfSecureMediaEnabled < ActiveRecord::Migration[7.0]
+ ##
+ # At this point in time, secure media is not compatible with chat,
+ # so if it is enabled then chat uploads must be disabled to avoid undesirable
+ # behaviour.
+ #
+ # The env var DISCOURSE_ALLOW_UNSECURE_CHAT_UPLOADS can be set to keep
+ # it enabled, but this is strongly advised against.
+ def up
+ chat_allow_uploads_value =
+ DB.query_single("SELECT value FROM site_settings WHERE name = 'chat_allow_uploads'").first
+
+ # nil means it is true, since the default value is true
+ chat_uploads_enabled = chat_allow_uploads_value == "t" || chat_allow_uploads_value.nil?
+
+ secure_media_enabled =
+ DB.query_single("SELECT value FROM site_settings WHERE name = 'secure_media'").first == "t"
+ secure_uploads_enabled =
+ DB.query_single("SELECT value FROM site_settings WHERE name = 'secure_uploads'").first == "t"
+
+ if (secure_media_enabled || secure_uploads_enabled) && chat_uploads_enabled &&
+ !GlobalSetting.allow_unsecure_chat_uploads
+ if chat_allow_uploads_value.nil?
+ DB.exec(
+ "
+ INSERT INTO site_settings(name, data_type, value, created_at, updated_at)
+ VALUES('chat_allow_uploads', 5, 'f', NOW(), NOW())
+ ",
+ )
+ else
+ DB.exec("UPDATE site_settings SET value = 'f' WHERE name = 'chat_allow_uploads'")
+ end
+ end
+ end
+
+ def down
+ raise ActiveRecord::IrreversibleMigration
+ end
+end
diff --git a/plugins/chat/db/migrate/20220901034107_add_user_count_stale_to_channel.rb b/plugins/chat/db/migrate/20220901034107_add_user_count_stale_to_channel.rb
new file mode 100644
index 0000000000..b62fee3254
--- /dev/null
+++ b/plugins/chat/db/migrate/20220901034107_add_user_count_stale_to_channel.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddUserCountStaleToChannel < ActiveRecord::Migration[7.0]
+ def change
+ add_column :chat_channels, :user_count_stale, :boolean, default: false, null: false
+ end
+end
diff --git a/plugins/chat/db/migrate/20221005143622_add_type_to_chat_channel.rb b/plugins/chat/db/migrate/20221005143622_add_type_to_chat_channel.rb
new file mode 100644
index 0000000000..236c1044f0
--- /dev/null
+++ b/plugins/chat/db/migrate/20221005143622_add_type_to_chat_channel.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddTypeToChatChannel < ActiveRecord::Migration[7.0]
+ def change
+ add_column :chat_channels, :type, :string
+ end
+end
diff --git a/plugins/chat/db/migrate/20221014005208_add_slug_column_to_chat_channel.rb b/plugins/chat/db/migrate/20221014005208_add_slug_column_to_chat_channel.rb
new file mode 100644
index 0000000000..ac9aa99814
--- /dev/null
+++ b/plugins/chat/db/migrate/20221014005208_add_slug_column_to_chat_channel.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddSlugColumnToChatChannel < ActiveRecord::Migration[7.0]
+ def change
+ add_column :chat_channels, :slug, :string
+
+ add_index :chat_channels, :slug
+ end
+end
diff --git a/plugins/chat/db/migrate/20221101061319_add_last_editor_id_to_chat_messages.rb b/plugins/chat/db/migrate/20221101061319_add_last_editor_id_to_chat_messages.rb
new file mode 100644
index 0000000000..feb782b127
--- /dev/null
+++ b/plugins/chat/db/migrate/20221101061319_add_last_editor_id_to_chat_messages.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class AddLastEditorIdToChatMessages < ActiveRecord::Migration[7.0]
+ def change
+ add_column :chat_messages, :last_editor_id, :integer
+ add_column :chat_message_revisions, :user_id, :integer
+
+ add_index :chat_messages, :last_editor_id
+ add_index :chat_message_revisions, :user_id
+ end
+end
diff --git a/plugins/chat/db/migrate/20221107034541_make_chat_editor_ids_not_null.rb b/plugins/chat/db/migrate/20221107034541_make_chat_editor_ids_not_null.rb
new file mode 100644
index 0000000000..04fd6c79bd
--- /dev/null
+++ b/plugins/chat/db/migrate/20221107034541_make_chat_editor_ids_not_null.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class MakeChatEditorIdsNotNull < ActiveRecord::Migration[7.0]
+ def change
+ DB.exec("UPDATE chat_messages SET last_editor_id = user_id")
+ DB.exec(<<~SQL)
+ UPDATE chat_message_revisions cmr
+ SET user_id = cm.user_id
+ FROM chat_messages AS cm
+ WHERE cmr.chat_message_id = cm.id
+ SQL
+
+ change_column_null :chat_messages, :last_editor_id, false
+ change_column_null :chat_message_revisions, :user_id, false
+ end
+end
diff --git a/plugins/chat/db/post_migrate/20220104051326_change_chat_channels_timestamp_columns_to_timestamp_type.rb b/plugins/chat/db/post_migrate/20220104051326_change_chat_channels_timestamp_columns_to_timestamp_type.rb
new file mode 100644
index 0000000000..ed4a829d4a
--- /dev/null
+++ b/plugins/chat/db/post_migrate/20220104051326_change_chat_channels_timestamp_columns_to_timestamp_type.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class ChangeChatChannelsTimestampColumnsToTimestampType < ActiveRecord::Migration[6.1]
+ def change
+ change_column_default :chat_channels, :created_at, nil
+ change_column_default :chat_channels, :updated_at, nil
+
+ # the earlier AddTimestampsToChatChannels migration has been modified,
+ # originally it added the columns as :datetime types, now it has been
+ # changed to use the correct :timestamp type, this exists check is here so
+ # we only try and make this change on old tables created before
+ if !column_exists?(:chat_channels, :created_at, :timestamp)
+ change_column :chat_channels, :created_at, :timestamp
+ end
+ if !column_exists?(:chat_channels, :updated_at, :timestamp)
+ change_column :chat_channels, :updated_at, :timestamp
+ end
+ end
+end
diff --git a/plugins/chat/db/post_migrate/20220321235638_drop_chat_message_post_connections_table.rb b/plugins/chat/db/post_migrate/20220321235638_drop_chat_message_post_connections_table.rb
new file mode 100644
index 0000000000..ca6d3b7957
--- /dev/null
+++ b/plugins/chat/db/post_migrate/20220321235638_drop_chat_message_post_connections_table.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require "migration/table_dropper"
+
+class DropChatMessagePostConnectionsTable < ActiveRecord::Migration[6.1]
+ def up
+ Migration::TableDropper.execute_drop("chat_message_post_connections")
+ end
+
+ def down
+ raise ActiveRecord::IrreversibleMigration
+ end
+end
diff --git a/plugins/chat/db/post_migrate/20220504080457_drop_old_chat_message_post_id_action_code_columns.rb b/plugins/chat/db/post_migrate/20220504080457_drop_old_chat_message_post_id_action_code_columns.rb
new file mode 100644
index 0000000000..6ca1c406b8
--- /dev/null
+++ b/plugins/chat/db/post_migrate/20220504080457_drop_old_chat_message_post_id_action_code_columns.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class DropOldChatMessagePostIdActionCodeColumns < ActiveRecord::Migration[7.0]
+ DROPPED_COLUMNS ||= { chat_messages: %i[post_id action_code] }
+
+ def up
+ DROPPED_COLUMNS.each { |table, columns| Migration::ColumnDropper.execute_drop(table, columns) }
+ end
+
+ def down
+ raise ActiveRecord::IrreversibleMigration
+ end
+end
diff --git a/plugins/chat/db/post_migrate/20220516142658_remove_email_statuses_table.rb b/plugins/chat/db/post_migrate/20220516142658_remove_email_statuses_table.rb
new file mode 100644
index 0000000000..4daf38ae0b
--- /dev/null
+++ b/plugins/chat/db/post_migrate/20220516142658_remove_email_statuses_table.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class RemoveEmailStatusesTable < ActiveRecord::Migration[7.0]
+ def up
+ remove_index :chat_message_email_statuses, :status
+ remove_index :chat_message_email_statuses, %i[user_id chat_message_id]
+
+ Migration::TableDropper.execute_drop("chat_message_email_statuses")
+ end
+
+ def down
+ raise ActiveRecord::IrreversibleMigration
+ end
+end
diff --git a/plugins/chat/db/post_migrate/20220518180642_remove_user_option_last_emailed_at.rb b/plugins/chat/db/post_migrate/20220518180642_remove_user_option_last_emailed_at.rb
new file mode 100644
index 0000000000..d55b9eec23
--- /dev/null
+++ b/plugins/chat/db/post_migrate/20220518180642_remove_user_option_last_emailed_at.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class RemoveUserOptionLastEmailedAt < ActiveRecord::Migration[7.0]
+ def change
+ remove_column :user_options, :last_emailed_for_chat, :datetime
+ end
+end
diff --git a/plugins/chat/db/post_migrate/20220526135414_remove_corrupted_last_read_message_id.rb b/plugins/chat/db/post_migrate/20220526135414_remove_corrupted_last_read_message_id.rb
new file mode 100644
index 0000000000..afcee809a4
--- /dev/null
+++ b/plugins/chat/db/post_migrate/20220526135414_remove_corrupted_last_read_message_id.rb
@@ -0,0 +1,109 @@
+# frozen_string_literal: true
+
+class RemoveCorruptedLastReadMessageId < ActiveRecord::Migration[7.0]
+ def down
+ raise ActiveRecord::IrreversibleMigration
+ end
+
+ def up
+ # Delete memberships for deleted channels
+ execute <<~SQL
+ DELETE FROM user_chat_channel_memberships uccm
+ WHERE NOT EXISTS (
+ SELECT FROM chat_channels cc
+ WHERE cc.id = uccm.chat_channel_id
+ );
+ SQL
+
+ # Delete messages for deleted channels
+ execute <<~SQL
+ DELETE FROM chat_messages cm
+ WHERE NOT EXISTS (
+ SELECT FROM chat_channels cc
+ WHERE cc.id = cm.chat_channel_id
+ );
+ SQL
+
+ # Reset highest_channel_message_id if the message cannot be found in the channel
+ execute <<~SQL
+ WITH highest_channel_message_id AS (
+ SELECT chat_channel_id, max(chat_messages.id) as highest_id
+ FROM chat_messages
+ GROUP BY chat_channel_id
+ )
+ UPDATE user_chat_channel_memberships uccm
+ SET last_read_message_id = highest_channel_message_id.highest_id
+ FROM highest_channel_message_id
+ WHERE highest_channel_message_id.chat_channel_id = uccm.chat_channel_id
+ AND uccm.last_read_message_id IS NOT NULL
+ AND uccm.last_read_message_id NOT IN (
+ SELECT id FROM chat_messages WHERE chat_messages.chat_channel_id = uccm.chat_channel_id
+ )
+ SQL
+
+ # Nullify in_reply_to where message is deleted
+ execute <<~SQL
+ UPDATE chat_messages cm
+ SET in_reply_to_id = NULL
+ WHERE NOT EXISTS (
+ SELECT FROM chat_messages cm2
+ WHERE cm.in_reply_to_id = cm2.id
+ );
+ SQL
+
+ # Delete chat_message_revisions with no message linked
+ execute <<~SQL
+ DELETE FROM chat_message_revisions cmr
+ WHERE NOT EXISTS (
+ SELECT FROM chat_messages cm
+ WHERE cm.id = cmr.chat_message_id
+ );
+ SQL
+
+ # Delete chat_message_reactions with no message linked
+ execute <<~SQL
+ DELETE FROM chat_message_reactions cmr
+ WHERE NOT EXISTS (
+ SELECT FROM chat_messages cm
+ WHERE cm.id = cmr.chat_message_id
+ );
+ SQL
+
+ # Delete bookmarks with no message linked
+ execute <<~SQL
+ DELETE FROM bookmarks b
+ WHERE b.bookmarkable_type = 'ChatMessage'
+ AND NOT EXISTS (
+ SELECT FROM chat_messages cm
+ WHERE cm.id = b.bookmarkable_id
+ );
+ SQL
+
+ # Delete chat_mention with no message linked
+ execute <<~SQL
+ DELETE FROM chat_mentions
+ WHERE NOT EXISTS (
+ SELECT FROM chat_messages cm
+ WHERE cm.id = chat_mentions.chat_message_id
+ );
+ SQL
+
+ # Delete chat_webhook_event with no message linked
+ execute <<~SQL
+ DELETE FROM chat_webhook_events cwe
+ WHERE NOT EXISTS (
+ SELECT FROM chat_messages cm
+ WHERE cm.id = cwe.chat_message_id
+ );
+ SQL
+
+ # Delete chat_uploads with no message linked
+ execute <<~SQL
+ DELETE FROM chat_uploads
+ WHERE NOT EXISTS (
+ SELECT FROM chat_messages cm
+ WHERE cm.id = chat_uploads.chat_message_id
+ );
+ SQL
+ end
+end
diff --git a/plugins/chat/db/post_migrate/20220531105951_drop_user_chat_channel_last_reads.rb b/plugins/chat/db/post_migrate/20220531105951_drop_user_chat_channel_last_reads.rb
new file mode 100644
index 0000000000..b746266d85
--- /dev/null
+++ b/plugins/chat/db/post_migrate/20220531105951_drop_user_chat_channel_last_reads.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require "migration/table_dropper"
+
+# usage has been dropped in https://github.com/discourse/discourse-chat/commit/1c110b71b28411dc7ac3ab9e3950e0bbf38d7970
+# but table never got dropped
+class DropUserChatChannelLastReads < ActiveRecord::Migration[7.0]
+ DROPPED_TABLES ||= %i[user_chat_channel_last_reads]
+
+ def up
+ DROPPED_TABLES.each { |table| Migration::TableDropper.execute_drop(table) }
+ end
+
+ def down
+ raise ActiveRecord::IrreversibleMigration
+ end
+end
diff --git a/plugins/chat/db/post_migrate/20220630074200_drop_chat_isolated_from_user_options.rb b/plugins/chat/db/post_migrate/20220630074200_drop_chat_isolated_from_user_options.rb
new file mode 100644
index 0000000000..0a41149418
--- /dev/null
+++ b/plugins/chat/db/post_migrate/20220630074200_drop_chat_isolated_from_user_options.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class DropChatIsolatedFromUserOptions < ActiveRecord::Migration[7.0]
+ DROPPED_COLUMNS ||= { user_options: %i[chat_isolated] }
+
+ def up
+ DROPPED_COLUMNS.each { |table, columns| Migration::ColumnDropper.execute_drop(table, columns) }
+ end
+
+ def down
+ raise ActiveRecord::IrreversibleMigration
+ end
+end
diff --git a/plugins/chat/db/post_migrate/20220701195731_convert_chatable_topics_to_categories.rb b/plugins/chat/db/post_migrate/20220701195731_convert_chatable_topics_to_categories.rb
new file mode 100644
index 0000000000..05e61f5f82
--- /dev/null
+++ b/plugins/chat/db/post_migrate/20220701195731_convert_chatable_topics_to_categories.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+class ConvertChatableTopicsToCategories < ActiveRecord::Migration[7.0]
+ def up
+ # convert chatable topics to categories using topic's category_id or default category
+ DB.exec(<<~SQL, uncategorized_category_id: SiteSetting.uncategorized_category_id)
+ UPDATE chat_channels cc
+ SET (chatable_type, chatable_id, name) = (
+ SELECT 'Category', coalesce(t.category_id, :uncategorized_category_id), coalesce(cc.name, t.title)
+ FROM topics t
+ WHERE cc.chatable_id = t.id
+ )
+ WHERE cc.chatable_type = 'Topic'
+ SQL
+
+ # soft delete all posts small actions
+ DB.exec(
+ "UPDATE posts SET deleted_at = :deleted_at, deleted_by_id = :deleted_by_id WHERE action_code IN (:action_codes)",
+ action_codes: %w[chat.enabled chat.disabled],
+ deleted_at: Time.zone.now,
+ deleted_by_id: Discourse::SYSTEM_USER_ID,
+ )
+
+ # removes all chat custom fields
+ DB.exec(<<~SQL)
+ DELETE FROM topic_custom_fields
+ WHERE name = 'has_chat_enabled'
+ SQL
+ end
+
+ def down
+ raise ActiveRecord::IrreversibleMigration
+ end
+end
diff --git a/plugins/chat/db/post_migrate/20221004122254_delete_reviewables_targetting_deleted_chat_messages.rb b/plugins/chat/db/post_migrate/20221004122254_delete_reviewables_targetting_deleted_chat_messages.rb
new file mode 100644
index 0000000000..bab35b96f7
--- /dev/null
+++ b/plugins/chat/db/post_migrate/20221004122254_delete_reviewables_targetting_deleted_chat_messages.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+class DeleteReviewablesTargettingDeletedChatMessages < ActiveRecord::Migration[7.0]
+ def down
+ raise ActiveRecord::IrreversibleMigration
+ end
+
+ def up
+ deleted_ids = DB.query_single <<~SQL
+ DELETE FROM reviewables r
+ WHERE r.type = 'ReviewableChatMessage'
+ AND r.id IN (
+ SELECT raux.id
+ FROM reviewables raux
+ LEFT OUTER JOIN chat_messages cm ON cm.id = raux.target_id
+ WHERE raux.type = 'ReviewableChatMessage' AND cm.id IS NULL
+ )
+ RETURNING r.id
+ SQL
+
+ if deleted_ids
+ DB.exec(<<~SQL, deleted_ids: deleted_ids)
+ DELETE FROM reviewable_scores rs
+ WHERE rs.reviewable_id IN (:deleted_ids)
+ SQL
+
+ DB.exec(<<~SQL, deleted_ids: deleted_ids)
+ DELETE FROM reviewable_histories rh
+ WHERE rh.reviewable_id IN (:deleted_ids)
+ SQL
+ end
+ end
+end
diff --git a/plugins/chat/db/post_migrate/20221018091412_migrate_chat_channels.rb b/plugins/chat/db/post_migrate/20221018091412_migrate_chat_channels.rb
new file mode 100644
index 0000000000..8a879b3a55
--- /dev/null
+++ b/plugins/chat/db/post_migrate/20221018091412_migrate_chat_channels.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+class MigrateChatChannels < ActiveRecord::Migration[7.0]
+ def up
+ DB.exec("UPDATE chat_channels SET type='CategoryChannel' WHERE chatable_type = 'Category'")
+ DB.exec(
+ "UPDATE chat_channels SET type='DMChannel' WHERE chatable_type = 'DirectMessageChannel'",
+ )
+ end
+end
diff --git a/plugins/chat/db/post_migrate/20221027090832_migrate_dm_channels.rb b/plugins/chat/db/post_migrate/20221027090832_migrate_dm_channels.rb
new file mode 100644
index 0000000000..f2510486d6
--- /dev/null
+++ b/plugins/chat/db/post_migrate/20221027090832_migrate_dm_channels.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class MigrateDmChannels < ActiveRecord::Migration[7.0]
+ def up
+ DB.exec(
+ "UPDATE chat_channels SET type='DirectMessageChannel', chatable_type='DirectMessage' WHERE chatable_type = 'DirectMessageChannel'",
+ )
+ end
+end
diff --git a/plugins/chat/db/post_migrate/20221104054957_backfill_channel_slugs.rb b/plugins/chat/db/post_migrate/20221104054957_backfill_channel_slugs.rb
new file mode 100644
index 0000000000..881ff9f04d
--- /dev/null
+++ b/plugins/chat/db/post_migrate/20221104054957_backfill_channel_slugs.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+class BackfillChannelSlugs < ActiveRecord::Migration[7.0]
+ def up
+ channels = DB.query(<<~SQL)
+ SELECT chat_channels.id, COALESCE(chat_channels.name, categories.name) AS title, NULL as slug
+ FROM chat_channels
+ INNER JOIN categories ON categories.id = chat_channels.chatable_id
+ WHERE chat_channels.chatable_type = 'Category' AND chat_channels.slug IS NULL
+ SQL
+ return if channels.count.zero?
+
+ DB.exec("CREATE TEMPORARY TABLE tmp_chat_channel_slugs(id int, slug text)")
+
+ taken_slugs = {}
+ channels.each do |channel|
+ # Simplified version of Slug.for generation that doesn't take into
+ # account different encodings to make things a little easier.
+ title = channel.title
+ if title.blank?
+ channel.slug = "channel-#{channel.id}"
+ else
+ channel.slug =
+ title
+ .downcase
+ .chomp
+ .tr("'", "")
+ .parameterize
+ .tr("_", "-")
+ .truncate(255, omission: "")
+ .squeeze("-")
+ .gsub(/\A-+|-+\z/, "")
+ end
+
+ # Deduplicate slugs with the channel IDs, we can always improve
+ # slugs later on.
+ channel.slug = "#{channel.slug}-#{channel.id}" if taken_slugs.key?(channel.slug)
+ taken_slugs[channel.slug] = true
+ end
+
+ values_to_insert =
+ channels.map { |channel| "(#{channel.id}, '#{PG::Connection.escape_string(channel.slug)}')" }
+
+ DB.exec(
+ "INSERT INTO tmp_chat_channel_slugs
+ VALUES #{values_to_insert.join(",\n")}",
+ )
+
+ DB.exec(<<~SQL)
+ UPDATE chat_channels cc
+ SET slug = tmp.slug
+ FROM tmp_chat_channel_slugs tmp
+ WHERE cc.id = tmp.id AND cc.slug IS NULL
+ SQL
+
+ DB.exec("DROP TABLE tmp_chat_channel_slugs")
+ end
+
+ def down
+ raise ActiveRecord::IrreversibleMigration
+ end
+end
diff --git a/plugins/chat/lib/chat_channel_archive_service.rb b/plugins/chat/lib/chat_channel_archive_service.rb
new file mode 100644
index 0000000000..8d1e0e65e0
--- /dev/null
+++ b/plugins/chat/lib/chat_channel_archive_service.rb
@@ -0,0 +1,247 @@
+# frozen_string_literal: true
+
+##
+# From time to time, site admins may choose to sunset a chat channel and archive
+# the messages within. The main use case for this is a topic-based channel, but
+# it can be used for category channels just fine. It cannot be used for DM channels
+# in its current iteration.
+#
+# To archive a channel, we mark it read_only first to prevent any further message
+# additions or changes, and create a record to track whether the archive topic
+# will be new or existing. When we archive the channel, messages are copied into
+# posts in batches using the [chat] BBCode to quote the messages. The messages are
+# deleted once the batch has its post made. The execute action of this class is
+# idempotent, so if we fail halfway through the archive process it can be run again.
+#
+# Once all of the messages have been copied then we mark the channel as archived.
+class Chat::ChatChannelArchiveService
+ ARCHIVED_MESSAGES_PER_POST = 100
+
+ def self.begin_archive_process(chat_channel:, acting_user:, topic_params:)
+ return if ChatChannelArchive.exists?(chat_channel: chat_channel)
+
+ ChatChannelArchive.transaction do
+ chat_channel.read_only!(acting_user)
+
+ archive =
+ ChatChannelArchive.create!(
+ chat_channel: chat_channel,
+ archived_by: acting_user,
+ total_messages: chat_channel.chat_messages.count,
+ destination_topic_id: topic_params[:topic_id],
+ destination_topic_title: topic_params[:topic_title],
+ destination_category_id: topic_params[:category_id],
+ destination_tags: topic_params[:tags],
+ )
+ Jobs.enqueue(:chat_channel_archive, chat_channel_archive_id: archive.id)
+
+ archive
+ end
+ end
+
+ def self.retry_archive_process(chat_channel:)
+ return if !chat_channel.chat_channel_archive&.failed?
+ Jobs.enqueue(
+ :chat_channel_archive,
+ chat_channel_archive_id: chat_channel.chat_channel_archive.id,
+ )
+ end
+
+ attr_reader :chat_channel_archive, :chat_channel, :chat_channel_title
+
+ def initialize(chat_channel_archive)
+ @chat_channel_archive = chat_channel_archive
+ @chat_channel = chat_channel_archive.chat_channel
+ @chat_channel_title = chat_channel.title(chat_channel_archive.archived_by)
+ end
+
+ def execute
+ chat_channel_archive.update(archive_error: nil)
+
+ begin
+ ensure_destination_topic_exists!
+
+ Rails.logger.info(
+ "Creating posts from message batches for #{chat_channel_title} archive, #{chat_channel_archive.total_messages} messages to archive (#{chat_channel_archive.total_messages / ARCHIVED_MESSAGES_PER_POST} posts).",
+ )
+
+ # a batch should be idempotent, either the post is created and the
+ # messages are deleted or we roll back the whole thing.
+ #
+ # at some point we may want to reconsider disabling post validations,
+ # and add in things like dynamic resizing of the number of messages per
+ # post based on post length, but that can be done later
+ #
+ # another future improvement is to send a MessageBus message for each
+ # completed batch, so the UI can receive updates and show a progress
+ # bar or something similar
+ chat_channel
+ .chat_messages
+ .find_in_batches(batch_size: ARCHIVED_MESSAGES_PER_POST) do |chat_messages|
+ create_post(
+ ChatTranscriptService.new(
+ chat_channel,
+ chat_channel_archive.archived_by,
+ messages_or_ids: chat_messages,
+ opts: {
+ no_link: true,
+ include_reactions: true,
+ },
+ ).generate_markdown,
+ ) { delete_message_batch(chat_messages.map(&:id)) }
+ end
+
+ kick_all_users
+ complete_archive
+ rescue => err
+ notify_archiver(:failed, error: err)
+ raise err
+ end
+ end
+
+ private
+
+ def create_post(raw)
+ pc = nil
+ Post.transaction do
+ pc =
+ PostCreator.new(
+ Discourse.system_user,
+ raw: raw,
+ # we must skip these because the posts are created in a big transaction,
+ # we do them all at the end instead
+ skip_jobs: true,
+ # we do not want to be sending out notifications etc. from this
+ # automatic background process
+ import_mode: true,
+ # don't want to be stopped by watched word or post length validations
+ skip_validations: true,
+ topic_id: chat_channel_archive.destination_topic_id,
+ )
+
+ pc.create
+
+ # so we can also delete chat messages in the same transaction
+ yield if block_given?
+ end
+ pc.enqueue_jobs
+ end
+
+ def ensure_destination_topic_exists!
+ if !chat_channel_archive.destination_topic.present?
+ Rails.logger.info("Creating topic for #{chat_channel_title} archive.")
+ Topic.transaction do
+ topic_creator =
+ TopicCreator.new(
+ Discourse.system_user,
+ Guardian.new(chat_channel_archive.archived_by),
+ {
+ title: chat_channel_archive.destination_topic_title,
+ category: chat_channel_archive.destination_category_id,
+ tags: chat_channel_archive.destination_tags,
+ import_mode: true,
+ },
+ )
+
+ chat_channel_archive.update!(destination_topic: topic_creator.create)
+ end
+
+ Rails.logger.info("Creating first post for #{chat_channel_title} archive.")
+ create_post(
+ I18n.t(
+ "chat.channel.archive.first_post_raw",
+ channel_name: chat_channel_title,
+ channel_url: chat_channel.url,
+ ),
+ )
+ else
+ Rails.logger.info("Topic already exists for #{chat_channel_title} archive.")
+ end
+
+ update_destination_topic_status
+ end
+
+ def update_destination_topic_status
+ # we only want to do this when the destination topic is new, not an
+ # existing topic, because we don't want to update the status unexpectedly
+ # on an existing topic
+ if chat_channel_archive.destination_topic_title.present?
+ if SiteSetting.chat_archive_destination_topic_status == "archived"
+ chat_channel_archive.destination_topic.update!(archived: true)
+ elsif SiteSetting.chat_archive_destination_topic_status == "closed"
+ chat_channel_archive.destination_topic.update!(closed: true)
+ end
+ end
+ end
+
+ def delete_message_batch(message_ids)
+ ChatMessage.transaction do
+ ChatMessage.where(id: message_ids).update_all(
+ deleted_at: DateTime.now,
+ deleted_by_id: chat_channel_archive.archived_by.id,
+ )
+
+ chat_channel_archive.update!(
+ archived_messages: chat_channel_archive.archived_messages + message_ids.length,
+ )
+ end
+
+ Rails.logger.info(
+ "Archived #{chat_channel_archive.archived_messages} messages for #{chat_channel_title} archive.",
+ )
+ end
+
+ def complete_archive
+ Rails.logger.info("Creating posts completed for #{chat_channel_title} archive.")
+ chat_channel.archived!(chat_channel_archive.archived_by)
+ notify_archiver(:success)
+ end
+
+ def notify_archiver(result, error: nil)
+ base_translation_params = {
+ channel_name: chat_channel_title,
+ topic_title: chat_channel_archive.destination_topic.title,
+ topic_url: chat_channel_archive.destination_topic.url,
+ }
+
+ if result == :failed
+ Discourse.warn_exception(
+ error,
+ message: "Error when archiving chat channel #{chat_channel_title}.",
+ env: {
+ chat_channel_id: chat_channel.id,
+ chat_channel_name: chat_channel_title,
+ },
+ )
+ error_translation_params =
+ base_translation_params.merge(
+ channel_url: chat_channel.url,
+ messages_archived: chat_channel_archive.archived_messages,
+ )
+ chat_channel_archive.update(archive_error: error.message)
+ SystemMessage.create_from_system_user(
+ chat_channel_archive.archived_by,
+ :chat_channel_archive_failed,
+ error_translation_params,
+ )
+ else
+ SystemMessage.create_from_system_user(
+ chat_channel_archive.archived_by,
+ :chat_channel_archive_complete,
+ base_translation_params,
+ )
+ end
+
+ ChatPublisher.publish_archive_status(
+ chat_channel,
+ archive_status: result,
+ archived_messages: chat_channel_archive.archived_messages,
+ archive_topic_id: chat_channel_archive.destination_topic_id,
+ total_messages: chat_channel_archive.total_messages,
+ )
+ end
+
+ def kick_all_users
+ Chat::ChatChannelMembershipManager.new(chat_channel).unfollow_all_users
+ end
+end
diff --git a/plugins/chat/lib/chat_channel_fetcher.rb b/plugins/chat/lib/chat_channel_fetcher.rb
new file mode 100644
index 0000000000..714737043f
--- /dev/null
+++ b/plugins/chat/lib/chat_channel_fetcher.rb
@@ -0,0 +1,221 @@
+# frozen_string_literal: true
+
+module Chat::ChatChannelFetcher
+ MAX_PUBLIC_CHANNEL_RESULTS = 50
+
+ def self.structured(guardian)
+ memberships = Chat::ChatChannelMembershipManager.all_for_user(guardian.user)
+ {
+ public_channels:
+ secured_public_channels(guardian, memberships, status: :open, following: true),
+ direct_message_channels:
+ secured_direct_message_channels(guardian.user.id, memberships, guardian),
+ memberships: memberships,
+ }
+ end
+
+ def self.all_secured_channel_ids(guardian, following: true)
+ allowed_channel_ids_sql = generate_allowed_channel_ids_sql(guardian)
+
+ return DB.query_single(allowed_channel_ids_sql) if !following
+
+ DB.query_single(<<~SQL, user_id: guardian.user.id)
+ SELECT chat_channel_id
+ FROM user_chat_channel_memberships
+ WHERE user_chat_channel_memberships.user_id = :user_id
+ AND user_chat_channel_memberships.chat_channel_id IN (
+ #{allowed_channel_ids_sql}
+ )
+ SQL
+ end
+
+ def self.generate_allowed_channel_ids_sql(guardian)
+ <<~SQL
+ -- secured category chat channels
+ #{
+ ChatChannel
+ .select(:id)
+ .joins(
+ "INNER JOIN categories ON categories.id = chat_channels.chatable_id AND chat_channels.chatable_type = 'Category'",
+ )
+ .where(
+ "categories.id IN (:allowed_category_ids)",
+ allowed_category_ids: guardian.allowed_category_ids,
+ )
+ .to_sql
+ }
+
+ UNION
+
+ -- secured direct message chat channels
+ #{
+ ChatChannel
+ .select(:id)
+ .joins(
+ "INNER JOIN direct_message_channels ON direct_message_channels.id = chat_channels.chatable_id
+ AND chat_channels.chatable_type = 'DirectMessage'
+ INNER JOIN direct_message_users ON direct_message_users.direct_message_channel_id = direct_message_channels.id",
+ )
+ .where("direct_message_users.user_id = :user_id", user_id: guardian.user.id)
+ .to_sql
+ }
+ SQL
+ end
+
+ def self.secured_public_channel_search(guardian, options = {})
+ channels =
+ ChatChannel
+ .includes(:chat_channel_archive)
+ .includes(chatable: [:topic_only_relative_url])
+ .joins(
+ "LEFT JOIN categories ON categories.id = chat_channels.chatable_id AND chat_channels.chatable_type = 'Category'",
+ )
+ .where(chatable_type: ChatChannel.public_channel_chatable_types)
+ .where("chat_channels.id IN (#{generate_allowed_channel_ids_sql(guardian)})")
+
+ channels = channels.where(status: options[:status]) if options[:status].present?
+
+ if options[:filter].present?
+ sql = "chat_channels.name ILIKE :filter OR categories.name ILIKE :filter"
+ channels =
+ channels.where(sql, filter: "%#{options[:filter].downcase}%").order(
+ "chat_channels.name ASC, categories.name ASC",
+ )
+ end
+
+ if options.key?(:following)
+ if options[:following]
+ channels =
+ channels.joins(:user_chat_channel_memberships).where(
+ user_chat_channel_memberships: {
+ user_id: guardian.user.id,
+ following: true,
+ },
+ )
+ else
+ channels =
+ channels.where(
+ "chat_channels.id NOT IN (SELECT chat_channel_id FROM user_chat_channel_memberships uccm WHERE uccm.chat_channel_id = chat_channels.id AND following IS TRUE AND user_id = ?)",
+ guardian.user.id,
+ )
+ end
+ end
+
+ options[:limit] = (options[:limit] || MAX_PUBLIC_CHANNEL_RESULTS).to_i.clamp(
+ 1,
+ MAX_PUBLIC_CHANNEL_RESULTS,
+ )
+ options[:offset] = [options[:offset].to_i, 0].max
+
+ channels.limit(options[:limit]).offset(options[:offset])
+ end
+
+ def self.secured_public_channels(guardian, memberships, options = { following: true })
+ channels = secured_public_channel_search(guardian, options)
+ decorate_memberships_with_tracking_data(guardian, channels, memberships)
+ channels = channels.to_a
+ preload_custom_fields_for(channels)
+ channels
+ end
+
+ def self.preload_custom_fields_for(channels)
+ preload_fields = Category.instance_variable_get(:@custom_field_types).keys
+ Category.preload_custom_fields(
+ channels.select { |c| c.chatable_type == "Category" }.map(&:chatable),
+ preload_fields,
+ )
+ end
+
+ def self.secured_direct_message_channels(user_id, memberships, guardian)
+ query = ChatChannel.includes(chatable: [{ direct_message_users: :user }, :users])
+ query = query.includes(chatable: [{ users: :user_status }]) if SiteSetting.enable_user_status
+
+ channels =
+ query
+ .joins(:user_chat_channel_memberships)
+ .where(user_chat_channel_memberships: { user_id: user_id, following: true })
+ .where(chatable_type: "DirectMessage")
+ .where("chat_channels.id IN (#{generate_allowed_channel_ids_sql(guardian)})")
+ .order(last_message_sent_at: :desc)
+ .to_a
+
+ preload_fields =
+ User.allowed_user_custom_fields(guardian) +
+ UserField.all.pluck(:id).map { |fid| "#{User::USER_FIELD_PREFIX}#{fid}" }
+ User.preload_custom_fields(channels.map { |c| c.chatable.users }.flatten, preload_fields)
+
+ decorate_memberships_with_tracking_data(guardian, channels, memberships)
+ end
+
+ def self.decorate_memberships_with_tracking_data(guardian, channels, memberships)
+ unread_counts_per_channel = unread_counts(channels, guardian.user.id)
+
+ mention_notifications =
+ Notification.unread.where(
+ user_id: guardian.user.id,
+ notification_type: Notification.types[:chat_mention],
+ )
+ mention_notification_data = mention_notifications.map { |m| JSON.parse(m.data) }
+
+ channels.each do |channel|
+ membership = memberships.find { |m| m.chat_channel_id == channel.id }
+
+ if membership
+ membership.unread_mentions =
+ mention_notification_data.count do |data|
+ data["chat_channel_id"] == channel.id &&
+ data["chat_message_id"] > (membership.last_read_message_id || 0)
+ end
+
+ membership.unread_count = unread_counts_per_channel[channel.id] if !membership.muted
+ end
+ end
+ end
+
+ def self.unread_counts(channels, user_id)
+ unread_counts = DB.query_array(<<~SQL, channel_ids: channels.map(&:id), user_id: user_id).to_h
+ SELECT cc.id, COUNT(*) as count
+ FROM chat_messages cm
+ JOIN chat_channels cc ON cc.id = cm.chat_channel_id
+ JOIN user_chat_channel_memberships uccm ON uccm.chat_channel_id = cc.id
+ WHERE cc.id IN (:channel_ids)
+ AND cm.user_id != :user_id
+ AND uccm.user_id = :user_id
+ AND cm.id > COALESCE(uccm.last_read_message_id, 0)
+ AND cm.deleted_at IS NULL
+ GROUP BY cc.id
+ SQL
+ unread_counts.default = 0
+ unread_counts
+ end
+
+ def self.find_with_access_check(channel_id_or_name, guardian)
+ begin
+ channel_id_or_name = Integer(channel_id_or_name)
+ rescue ArgumentError
+ end
+
+ base_channel_relation =
+ ChatChannel.includes(:chatable).joins(
+ "LEFT JOIN categories ON categories.id = chat_channels.chatable_id AND chat_channels.chatable_type = 'Category'",
+ )
+
+ if guardian.user.staff?
+ base_channel_relation = base_channel_relation.includes(:chat_channel_archive)
+ end
+
+ if channel_id_or_name.is_a? Integer
+ chat_channel = base_channel_relation.find_by(id: channel_id_or_name)
+ else
+ chat_channel =
+ base_channel_relation.find_by(
+ "LOWER(categories.name) = :name OR LOWER(chat_channels.name) = :name",
+ name: channel_id_or_name.downcase,
+ )
+ end
+
+ raise Discourse::NotFound if chat_channel.blank?
+ raise Discourse::InvalidAccess if !guardian.can_see_chat_channel?(chat_channel)
+ chat_channel
+ end
+end
diff --git a/plugins/chat/lib/chat_channel_membership_manager.rb b/plugins/chat/lib/chat_channel_membership_manager.rb
new file mode 100644
index 0000000000..5947f23d7a
--- /dev/null
+++ b/plugins/chat/lib/chat_channel_membership_manager.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+class Chat::ChatChannelMembershipManager
+ def self.all_for_user(user)
+ UserChatChannelMembership.where(user: user)
+ end
+
+ attr_reader :channel
+
+ def initialize(channel)
+ @channel = channel
+ end
+
+ def find_for_user(user, following: nil)
+ params = { user_id: user.id, chat_channel_id: channel.id }
+ params[:following] = following if following.present?
+
+ UserChatChannelMembership.includes(:user, :chat_channel).find_by(params)
+ end
+
+ def follow(user)
+ membership =
+ find_for_user(user) ||
+ UserChatChannelMembership.new(user: user, chat_channel: channel, following: true)
+
+ ActiveRecord::Base.transaction do
+ if membership.new_record?
+ membership.save!
+ recalculate_user_count
+ elsif !membership.following
+ membership.update!(following: true)
+ recalculate_user_count
+ end
+ end
+
+ membership
+ end
+
+ def unfollow(user)
+ membership = find_for_user(user)
+
+ return if membership.blank?
+
+ ActiveRecord::Base.transaction do
+ if membership.following
+ membership.update!(following: false)
+ recalculate_user_count
+ end
+ end
+
+ membership
+ end
+
+ def recalculate_user_count
+ return if ChatChannel.exists?(id: channel.id, user_count_stale: true)
+ channel.update!(user_count_stale: true)
+ Jobs.enqueue_in(3.seconds, :update_channel_user_count, chat_channel_id: channel.id)
+ end
+
+ def unfollow_all_users
+ UserChatChannelMembership.where(chat_channel: channel).update_all(
+ following: false,
+ last_read_message_id: channel.chat_messages.last&.id,
+ )
+ end
+
+ def enforce_automatic_channel_memberships
+ Jobs.enqueue(:auto_manage_channel_memberships, chat_channel_id: channel.id)
+ end
+
+ def enforce_automatic_user_membership(user)
+ Jobs.enqueue(
+ :auto_join_channel_batch,
+ chat_channel_id: channel.id,
+ starts_at: user.id,
+ ends_at: user.id,
+ )
+ end
+end
diff --git a/plugins/chat/lib/chat_mailer.rb b/plugins/chat/lib/chat_mailer.rb
new file mode 100644
index 0000000000..8c914b497d
--- /dev/null
+++ b/plugins/chat/lib/chat_mailer.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+class Chat::ChatMailer
+ def self.send_unread_mentions_summary
+ return unless SiteSetting.chat_enabled
+
+ users_with_unprocessed_unread_mentions.find_each do |user|
+ # user#memberships_with_unread_messages is a nested array that looks like [[membership_id, unread_message_id]]
+ # Find the max unread id per membership.
+ membership_and_max_unread_mention_ids =
+ user
+ .memberships_with_unread_messages
+ .group_by { |memberships| memberships[0] }
+ .transform_values do |membership_and_msg_ids|
+ membership_and_msg_ids.max_by { |membership, msg| msg }
+ end
+ .values
+
+ Jobs.enqueue(
+ :user_email,
+ type: "chat_summary",
+ user_id: user.id,
+ force_respect_seen_recently: true,
+ memberships_to_update_data: membership_and_max_unread_mention_ids,
+ )
+ end
+ end
+
+ private
+
+ def self.users_with_unprocessed_unread_mentions
+ when_away_frequency = UserOption.chat_email_frequencies[:when_away]
+ allowed_group_ids = Chat.allowed_group_ids
+
+ users = User
+ .joins(:user_option)
+ .where(user_options: { chat_enabled: true, chat_email_frequency: when_away_frequency })
+ .where("users.last_seen_at < ?", 15.minutes.ago)
+
+ if !allowed_group_ids.include?(Group::AUTO_GROUPS[:everyone])
+ users = users.joins(:groups).where(groups: { id: allowed_group_ids })
+ end
+
+ users
+ .select("users.id", "ARRAY_AGG(ARRAY[uccm.id, c_msg.id]) AS memberships_with_unread_messages")
+ .joins("INNER JOIN user_chat_channel_memberships uccm ON uccm.user_id = users.id")
+ .joins("INNER JOIN chat_channels cc ON cc.id = uccm.chat_channel_id")
+ .joins("INNER JOIN chat_messages c_msg ON c_msg.chat_channel_id = uccm.chat_channel_id")
+ .joins("LEFT OUTER JOIN chat_mentions c_mentions ON c_mentions.chat_message_id = c_msg.id")
+ .where("c_msg.deleted_at IS NULL AND c_msg.user_id <> users.id")
+ .where("c_msg.created_at > ?", 1.week.ago)
+ .where(<<~SQL)
+ (uccm.last_read_message_id IS NULL OR c_msg.id > uccm.last_read_message_id) AND
+ (uccm.last_unread_mention_when_emailed_id IS NULL OR c_msg.id > uccm.last_unread_mention_when_emailed_id) AND
+ (
+ (uccm.user_id = c_mentions.user_id AND uccm.following IS true AND cc.chatable_type = 'Category') OR
+ (cc.chatable_type = 'DirectMessage')
+ )
+ SQL
+ .group("users.id, uccm.user_id")
+ end
+end
diff --git a/plugins/chat/lib/chat_message_bookmarkable.rb b/plugins/chat/lib/chat_message_bookmarkable.rb
new file mode 100644
index 0000000000..8a72d0f9f2
--- /dev/null
+++ b/plugins/chat/lib/chat_message_bookmarkable.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+class ChatMessageBookmarkable < BaseBookmarkable
+ def self.model
+ ChatMessage
+ end
+
+ def self.serializer
+ UserChatMessageBookmarkSerializer
+ end
+
+ def self.preload_associations
+ [:chat_channel]
+ end
+
+ def self.list_query(user, guardian)
+ accessible_channel_ids = Chat::ChatChannelFetcher.all_secured_channel_ids(guardian)
+ return if accessible_channel_ids.empty?
+ user
+ .bookmarks_of_type("ChatMessage")
+ .joins(
+ "INNER JOIN chat_messages ON chat_messages.id = bookmarks.bookmarkable_id
+ AND chat_messages.deleted_at IS NULL
+ AND bookmarks.bookmarkable_type = 'ChatMessage'",
+ )
+ .where("chat_messages.chat_channel_id IN (?)", accessible_channel_ids)
+ end
+
+ def self.search_query(bookmarks, query, ts_query, &bookmarkable_search)
+ bookmarkable_search.call(bookmarks, "chat_messages.message ILIKE :q")
+ end
+
+ def self.validate_before_create(guardian, bookmarkable)
+ if bookmarkable.blank? || !guardian.can_see_chat_channel?(bookmarkable.chat_channel)
+ raise Discourse::InvalidAccess
+ end
+ end
+
+ def self.reminder_handler(bookmark)
+ send_reminder_notification(
+ bookmark,
+ data: {
+ title:
+ I18n.t(
+ "chat.bookmarkable.notification_title",
+ channel_name: bookmark.bookmarkable.chat_channel.title(bookmark.user),
+ ),
+ bookmarkable_url: bookmark.bookmarkable.url,
+ },
+ )
+ end
+
+ def self.reminder_conditions(bookmark)
+ bookmark.bookmarkable.present? && bookmark.bookmarkable.chat_channel.present?
+ end
+
+ def self.can_see?(guardian, bookmark)
+ guardian.can_see_chat_channel?(bookmark.bookmarkable.chat_channel)
+ end
+
+ def self.cleanup_deleted
+ DB.query(<<~SQL, grace_time: 3.days.ago)
+ DELETE FROM bookmarks b
+ USING chat_messages cm
+ WHERE b.bookmarkable_id = cm.id AND b.bookmarkable_type = 'ChatMessage'
+ AND (cm.deleted_at < :grace_time)
+ SQL
+ end
+end
diff --git a/plugins/chat/lib/chat_message_creator.rb b/plugins/chat/lib/chat_message_creator.rb
new file mode 100644
index 0000000000..49b9f58b9f
--- /dev/null
+++ b/plugins/chat/lib/chat_message_creator.rb
@@ -0,0 +1,102 @@
+# frozen_string_literal: true
+class Chat::ChatMessageCreator
+ attr_reader :error, :chat_message
+
+ def self.create(opts)
+ instance = new(**opts)
+ instance.create
+ instance
+ end
+
+ def initialize(
+ chat_channel:,
+ in_reply_to_id: nil,
+ user:,
+ content:,
+ staged_id: nil,
+ incoming_chat_webhook: nil,
+ upload_ids: nil
+ )
+ @chat_channel = chat_channel
+ @user = user
+ @guardian = Guardian.new(user)
+ @in_reply_to_id = in_reply_to_id
+ @content = content
+ @staged_id = staged_id
+ @incoming_chat_webhook = incoming_chat_webhook
+ @upload_ids = upload_ids || []
+ @error = nil
+
+ @chat_message =
+ ChatMessage.new(
+ chat_channel: @chat_channel,
+ user_id: @user.id,
+ in_reply_to_id: @in_reply_to_id,
+ message: @content,
+ )
+ end
+
+ def create
+ begin
+ validate_channel_status!
+ uploads = get_uploads
+ validate_message!(has_uploads: uploads.any?)
+ @chat_message.cook
+ @chat_message.save!
+ create_chat_webhook_event
+ @chat_message.attach_uploads(uploads)
+ ChatDraft.where(user_id: @user.id, chat_channel_id: @chat_channel.id).destroy_all
+ ChatPublisher.publish_new!(@chat_channel, @chat_message, @staged_id)
+ Jobs.enqueue(:process_chat_message, { chat_message_id: @chat_message.id })
+ Chat::ChatNotifier.notify_new(
+ chat_message: @chat_message,
+ timestamp: @chat_message.created_at,
+ )
+ DiscourseEvent.trigger(:chat_message_created, @chat_message, @chat_channel, @user)
+ rescue => error
+ @error = error
+ end
+ end
+
+ def failed?
+ @error.present?
+ end
+
+ private
+
+ def validate_channel_status!
+ return if @guardian.can_create_channel_message?(@chat_channel)
+
+ if @chat_channel.direct_message_channel? && !@guardian.can_create_direct_message?
+ raise StandardError.new(I18n.t("chat.errors.user_cannot_send_direct_messages"))
+ else
+ raise StandardError.new(
+ I18n.t(
+ "chat.errors.channel_new_message_disallowed",
+ status: @chat_channel.status_name,
+ ),
+ )
+ end
+ end
+
+ def validate_message!(has_uploads:)
+ @chat_message.validate_message(has_uploads: has_uploads)
+ if @chat_message.errors.present?
+ raise StandardError.new(@chat_message.errors.map(&:full_message).join(", "))
+ end
+ end
+
+ def create_chat_webhook_event
+ return if @incoming_chat_webhook.blank?
+ ChatWebhookEvent.create(
+ chat_message: @chat_message,
+ incoming_chat_webhook: @incoming_chat_webhook,
+ )
+ end
+
+ def get_uploads
+ return [] if @upload_ids.blank? || !SiteSetting.chat_allow_uploads
+
+ Upload.where(id: @upload_ids, user_id: @user.id)
+ end
+end
diff --git a/plugins/chat/lib/chat_message_processor.rb b/plugins/chat/lib/chat_message_processor.rb
new file mode 100644
index 0000000000..078e73cf0a
--- /dev/null
+++ b/plugins/chat/lib/chat_message_processor.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+class Chat::ChatMessageProcessor
+ include ::CookedProcessorMixin
+
+ def initialize(chat_message)
+ @model = chat_message
+ @previous_cooked = (chat_message.cooked || "").dup
+ @with_secure_uploads = false
+ @size_cache = {}
+ @opts = {}
+
+ cooked = ChatMessage.cook(chat_message.message)
+ @doc = Loofah.fragment(cooked)
+ end
+
+ def run!
+ post_process_oneboxes
+ DiscourseEvent.trigger(:chat_message_processed, @doc, @model)
+ end
+
+ def large_images
+ []
+ end
+
+ def broken_images
+ []
+ end
+
+ def downloaded_images
+ {}
+ end
+end
diff --git a/plugins/chat/lib/chat_message_rate_limiter.rb b/plugins/chat/lib/chat_message_rate_limiter.rb
new file mode 100644
index 0000000000..9f205098e7
--- /dev/null
+++ b/plugins/chat/lib/chat_message_rate_limiter.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+class Chat::ChatMessageRateLimiter
+ def self.run!(user)
+ instance = self.new(user)
+ instance.run!
+ end
+
+ def initialize(user)
+ @user = user
+ end
+
+ def run!
+ return if @user.staff?
+
+ allowed_message_count =
+ (
+ if @user.trust_level == TrustLevel[0]
+ SiteSetting.chat_allowed_messages_for_trust_level_0
+ else
+ SiteSetting.chat_allowed_messages_for_other_trust_levels
+ end
+ )
+ return if allowed_message_count.zero?
+
+ @rate_limiter = RateLimiter.new(@user, "create_chat_message", allowed_message_count, 30.seconds)
+ silence_user if @rate_limiter.remaining.zero?
+ @rate_limiter.performed!
+ end
+
+ def clear!
+ # Used only for testing. Need to clear the rate limiter between tests.
+ @rate_limiter.clear! if defined?(@rate_limiter)
+ end
+
+ private
+
+ def silence_user
+ silenced_for_minutes = SiteSetting.chat_auto_silence_duration
+ return unless silenced_for_minutes > 0
+
+ UserSilencer.silence(
+ @user,
+ Discourse.system_user,
+ silenced_till: silenced_for_minutes.minutes.from_now,
+ reason: I18n.t("chat.errors.rate_limit_exceeded"),
+ )
+ end
+end
diff --git a/plugins/chat/lib/chat_message_reactor.rb b/plugins/chat/lib/chat_message_reactor.rb
new file mode 100644
index 0000000000..9304809cad
--- /dev/null
+++ b/plugins/chat/lib/chat_message_reactor.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+class Chat::ChatMessageReactor
+ ADD_REACTION = :add
+ REMOVE_REACTION = :remove
+ MAX_REACTIONS_LIMIT = 30
+
+ def initialize(user, chat_channel)
+ @user = user
+ @chat_channel = chat_channel
+ @guardian = Guardian.new(user)
+ end
+
+ def react!(message_id:, react_action:, emoji:)
+ @guardian.ensure_can_see_chat_channel!(@chat_channel)
+ @guardian.ensure_can_react!
+ validate_channel_status!
+ validate_reaction!(react_action, emoji)
+ message = ensure_chat_message!(message_id)
+ validate_max_reactions!(message, react_action, emoji)
+
+ ActiveRecord::Base.transaction do
+ enforce_channel_membership!
+ create_reaction(message, react_action, emoji)
+ end
+
+ publish_reaction(message, react_action, emoji)
+
+ message
+ end
+
+ private
+
+ def ensure_chat_message!(message_id)
+ message = ChatMessage.find_by(id: message_id, chat_channel: @chat_channel)
+ raise Discourse::NotFound unless message
+ message
+ end
+
+ def validate_reaction!(react_action, emoji)
+ if ![ADD_REACTION, REMOVE_REACTION].include?(react_action) || !Emoji.exists?(emoji)
+ raise Discourse::InvalidParameters
+ end
+ end
+
+ def enforce_channel_membership!
+ Chat::ChatChannelMembershipManager.new(@chat_channel).follow(@user)
+ end
+
+ def validate_channel_status!
+ return if @guardian.can_create_channel_message?(@chat_channel)
+ raise Discourse::InvalidAccess.new(
+ nil,
+ nil,
+ custom_message: "chat.errors.channel_modify_message_disallowed",
+ custom_message_params: {
+ status: @chat_channel.status_name,
+ },
+ )
+ end
+
+ def validate_max_reactions!(message, react_action, emoji)
+ if react_action == ADD_REACTION &&
+ message.reactions.count("DISTINCT emoji") >= MAX_REACTIONS_LIMIT &&
+ !message.reactions.exists?(emoji: emoji)
+ raise Discourse::InvalidAccess.new(
+ nil,
+ nil,
+ custom_message: "chat.errors.max_reactions_limit_reached",
+ )
+ end
+ end
+
+ def create_reaction(message, react_action, emoji)
+ if react_action == ADD_REACTION
+ message.reactions.find_or_create_by!(user: @user, emoji: emoji)
+ else
+ message.reactions.where(user: @user, emoji: emoji).destroy_all
+ end
+ end
+
+ def publish_reaction(message, react_action, emoji)
+ ChatPublisher.publish_reaction!(@chat_channel, message, react_action, @user, emoji)
+ end
+end
diff --git a/plugins/chat/lib/chat_message_updater.rb b/plugins/chat/lib/chat_message_updater.rb
new file mode 100644
index 0000000000..e72bdb3d93
--- /dev/null
+++ b/plugins/chat/lib/chat_message_updater.rb
@@ -0,0 +1,96 @@
+# frozen_string_literal: true
+
+class Chat::ChatMessageUpdater
+ attr_reader :error
+
+ def self.update(opts)
+ instance = new(**opts)
+ instance.update
+ instance
+ end
+
+ def initialize(guardian:, chat_message:, new_content:, upload_ids: nil)
+ @guardian = guardian
+ @user = guardian.user
+ @chat_message = chat_message
+ @old_message_content = chat_message.message
+ @chat_channel = @chat_message.chat_channel
+ @new_content = new_content
+ @upload_ids = upload_ids
+ @error = nil
+ end
+
+ def update
+ begin
+ validate_channel_status!
+ @guardian.ensure_can_edit_chat!(@chat_message)
+ @chat_message.message = @new_content
+ @chat_message.last_editor_id = @user.id
+ upload_info = get_upload_info
+ validate_message!(has_uploads: upload_info[:uploads].any?)
+ @chat_message.cook
+ @chat_message.save!
+ update_uploads(upload_info)
+ revision = save_revision!
+ ChatPublisher.publish_edit!(@chat_channel, @chat_message)
+ Jobs.enqueue(:process_chat_message, { chat_message_id: @chat_message.id })
+ Chat::ChatNotifier.notify_edit(chat_message: @chat_message, timestamp: revision.created_at)
+ DiscourseEvent.trigger(:chat_message_edited, @chat_message, @chat_channel, @user)
+ rescue => error
+ @error = error
+ end
+ end
+
+ def failed?
+ @error.present?
+ end
+
+ private
+
+ def validate_channel_status!
+ return if @guardian.can_modify_channel_message?(@chat_channel)
+ raise StandardError.new(
+ I18n.t(
+ "chat.errors.channel_modify_message_disallowed",
+ status: @chat_channel.status_name,
+ ),
+ )
+ end
+
+ def validate_message!(has_uploads:)
+ @chat_message.validate_message(has_uploads: has_uploads)
+ if @chat_message.errors.present?
+ raise StandardError.new(@chat_message.errors.map(&:full_message).join(", "))
+ end
+ end
+
+ def get_upload_info
+ return { uploads: [] } if @upload_ids.nil? || !SiteSetting.chat_allow_uploads
+
+ uploads = Upload.where(id: @upload_ids, user_id: @user.id)
+ if uploads.count != @upload_ids.count
+ # User is passing upload_ids for uploads that they don't own. Don't change anything.
+ return { uploads: @chat_message.uploads, changed: false }
+ end
+
+ new_upload_ids = uploads.map(&:id)
+ existing_upload_ids = @chat_message.upload_ids
+ difference = (existing_upload_ids + new_upload_ids) - (existing_upload_ids & new_upload_ids)
+ { uploads: uploads, changed: difference.any? }
+ end
+
+ def update_uploads(upload_info)
+ return unless upload_info[:changed]
+
+ ChatUpload.where(chat_message: @chat_message).destroy_all
+ @chat_message.attach_uploads(upload_info[:uploads])
+ end
+
+ def save_revision!
+ @chat_message.revisions.create!(
+ old_message: @old_message_content,
+ new_message: @chat_message.message,
+ user_id: @user.id,
+ )
+ end
+end
diff --git a/plugins/chat/lib/chat_notifier.rb b/plugins/chat/lib/chat_notifier.rb
new file mode 100644
index 0000000000..d2fcc4496a
--- /dev/null
+++ b/plugins/chat/lib/chat_notifier.rb
@@ -0,0 +1,335 @@
+# frozen_string_literal: true
+
+##
+# When we are attempting to notify users based on a message we have to take
+# into account the following:
+#
+# * Individual user mentions like @alfred
+# * Group mentions that include N users such as @support
+# * Global @here and @all mentions
+# * Users watching the channel via UserChatChannelMembership
+#
+# For various reasons a mention may not notify a user:
+#
+# * The target user of the mention is ignoring or muting the user who created the message
+# * The target user either cannot chat or cannot see the chat channel, in which case
+# they are defined as `unreachable`
+# * The target user is not a member of the channel, in which case they are defined
+# as `welcome_to_join`
+# * In the case of global @here and @all mentions users with the preference
+# `ignore_channel_wide_mention` set to true will not be notified
+#
+# For any users that fall under the `unreachable` or `welcome_to_join` umbrellas
+# we send a MessageBus message to the UI and to inform the creating user. The
+# creating user can invite any `welcome_to_join` users to the channel. Target
+# users who are ignoring or muting the creating user _do not_ fall into this bucket.
+#
+# The ignore/mute filtering is also applied via the ChatNotifyWatching job,
+# which prevents desktop / push notifications being sent.
+class Chat::ChatNotifier
+ class << self
+ def user_has_seen_message?(membership, chat_message_id)
+ (membership.last_read_message_id || 0) >= chat_message_id
+ end
+
+ def push_notification_tag(type, chat_channel_id)
+ "#{Discourse.current_hostname}-chat-#{type}-#{chat_channel_id}"
+ end
+
+ def notify_edit(chat_message:, timestamp:)
+ new(chat_message, timestamp).notify_edit
+ end
+
+ def notify_new(chat_message:, timestamp:)
+ new(chat_message, timestamp).notify_new
+ end
+ end
+
+ def initialize(chat_message, timestamp)
+ @chat_message = chat_message
+ @timestamp = timestamp
+ @chat_channel = @chat_message.chat_channel
+ @user = @chat_message.user
+ end
+
+ ### Public API
+
+ def notify_new
+ to_notify = list_users_to_notify
+ inaccessible = to_notify.extract!(:unreachable, :welcome_to_join)
+ mentioned_user_ids = to_notify.extract!(:all_mentioned_user_ids)[:all_mentioned_user_ids]
+
+ mentioned_user_ids.each do |member_id|
+ ChatPublisher.publish_new_mention(member_id, @chat_channel.id, @chat_message.id)
+ end
+
+ notify_creator_of_inaccessible_mentions(
+ inaccessible[:unreachable],
+ inaccessible[:welcome_to_join],
+ )
+
+ notify_mentioned_users(to_notify)
+ notify_watching_users(except: mentioned_user_ids << @user.id)
+
+ to_notify
+ end
+
+ def notify_edit
+ existing_notifications =
+ ChatMention.includes(:user, :notification).where(chat_message: @chat_message)
+ already_notified_user_ids = existing_notifications.map(&:user_id)
+
+ to_notify = list_users_to_notify
+ inaccessible = to_notify.extract!(:unreachable, :welcome_to_join)
+ mentioned_user_ids = to_notify.extract!(:all_mentioned_user_ids)[:all_mentioned_user_ids]
+
+ needs_deletion = already_notified_user_ids - mentioned_user_ids
+ needs_deletion.each do |user_id|
+ chat_mention = existing_notifications.detect { |n| n.user_id == user_id }
+ chat_mention.notification.destroy!
+ chat_mention.destroy!
+ end
+
+ needs_notification_ids = mentioned_user_ids - already_notified_user_ids
+ return if needs_notification_ids.blank?
+
+ notify_creator_of_inaccessible_mentions(
+ inaccessible[:unreachable],
+ inaccessible[:welcome_to_join],
+ )
+
+ notify_mentioned_users(to_notify, already_notified_user_ids: already_notified_user_ids)
+
+ to_notify
+ end
+
+ private
+
+ def list_users_to_notify
+ {}.tap do |to_notify|
+ # The order of these methods is the precedence
+ # between different mention types.
+
+ already_covered_ids = []
+
+ expand_direct_mentions(to_notify, already_covered_ids)
+ expand_group_mentions(to_notify, already_covered_ids)
+ expand_here_mention(to_notify, already_covered_ids)
+ expand_global_mention(to_notify, already_covered_ids)
+
+ filter_users_ignoring_or_muting_creator(to_notify, already_covered_ids)
+
+ to_notify[:all_mentioned_user_ids] = already_covered_ids
+ end
+ end
+
+ def chat_users
+ users =
+ User.includes(:do_not_disturb_timings, :push_subscriptions, :user_chat_channel_memberships)
+
+ users
+ .distinct
+ .joins("LEFT OUTER JOIN user_chat_channel_memberships uccm ON uccm.user_id = users.id")
+ .joins(:user_option)
+ .real
+ .not_suspended
+ .where(user_options: { chat_enabled: true })
+ .where.not(username_lower: @user.username.downcase)
+ end
+
+ def rest_of_the_channel
+ chat_users.where(
+ user_chat_channel_memberships: {
+ following: true,
+ chat_channel_id: @chat_channel.id,
+ },
+ )
+ end
+
+ def members_accepting_channel_wide_notifications
+ rest_of_the_channel.where(user_options: { ignore_channel_wide_mention: [false, nil] })
+ end
+
+ def direct_mentions_from_cooked
+ @direct_mentions_from_cooked ||=
+ Nokogiri::HTML5.fragment(@chat_message.cooked).css(".mention").map(&:text)
+ end
+
+ def normalized_mentions(mentions)
+ mentions.reduce([]) do |memo, mention|
+ %w[@here @all].include?(mention.downcase) ? memo : (memo << mention[1..-1].downcase)
+ end
+ end
+
+ def expand_global_mention(to_notify, already_covered_ids)
+ typed_global_mention = direct_mentions_from_cooked.include?("@all")
+
+ if typed_global_mention
+ to_notify[:global_mentions] = members_accepting_channel_wide_notifications
+ .where.not(username_lower: normalized_mentions(direct_mentions_from_cooked))
+ .where.not(id: already_covered_ids)
+ .pluck(:id)
+
+ already_covered_ids.concat(to_notify[:global_mentions])
+ else
+ to_notify[:global_mentions] = []
+ end
+ end
+
+ def expand_here_mention(to_notify, already_covered_ids)
+ typed_here_mention = direct_mentions_from_cooked.include?("@here")
+
+ if typed_here_mention
+ to_notify[:here_mentions] = members_accepting_channel_wide_notifications
+ .where("last_seen_at > ?", 5.minutes.ago)
+ .where.not(username_lower: normalized_mentions(direct_mentions_from_cooked))
+ .where.not(id: already_covered_ids)
+ .pluck(:id)
+
+ already_covered_ids.concat(to_notify[:here_mentions])
+ else
+ to_notify[:here_mentions] = []
+ end
+ end
+
+ def group_users_to_notify(users)
+ potential_participants, unreachable =
+ users.partition do |user|
+ guardian = Guardian.new(user)
+ guardian.can_chat?(user) && guardian.can_see_chat_channel?(@chat_channel)
+ end
+
+ participants, welcome_to_join =
+ potential_participants.partition do |participant|
+ participant.user_chat_channel_memberships.any? do |m|
+ predicate = m.chat_channel_id == @chat_channel.id
+ predicate = predicate && m.following == true if @chat_channel.public_channel?
+ predicate
+ end
+ end
+
+ {
+ already_participating: participants || [],
+ welcome_to_join: welcome_to_join || [],
+ unreachable: unreachable || [],
+ }
+ end
+
+ def expand_direct_mentions(to_notify, already_covered_ids)
+ direct_mentions =
+ chat_users
+ .where(username_lower: normalized_mentions(direct_mentions_from_cooked))
+ .where.not(id: already_covered_ids)
+
+ grouped = group_users_to_notify(direct_mentions)
+
+ to_notify[:direct_mentions] = grouped[:already_participating].map(&:id)
+ to_notify[:welcome_to_join] = grouped[:welcome_to_join]
+ to_notify[:unreachable] = grouped[:unreachable]
+ already_covered_ids.concat(to_notify[:direct_mentions])
+ end
+
+ def group_name_mentions
+ @group_mentions_from_cooked ||=
+ normalized_mentions(
+ Nokogiri::HTML5.fragment(@chat_message.cooked).css(".mention-group").map(&:text),
+ )
+ end
+
+ def mentionable_groups
+ @mentionable_groups ||=
+ Group.mentionable(@user, include_public: false).where(
+ "LOWER(name) IN (?)",
+ group_name_mentions,
+ )
+ end
+
+ def expand_group_mentions(to_notify, already_covered_ids)
+ return [] if mentionable_groups.empty?
+
+ mentionable_groups.each { |g| to_notify[g.name.downcase] = [] }
+
+ reached_by_group =
+ chat_users.joins(:groups).where(groups: mentionable_groups).where.not(id: already_covered_ids)
+
+ grouped = group_users_to_notify(reached_by_group)
+
+ grouped[:already_participating].each do |user|
+ # When a user is a member of multiple mentioned groups,
+ # the most far to the left should take precedence.
+ ordered_group_names = group_name_mentions & mentionable_groups.map { |mg| mg.name.downcase }
+ user_group_names = user.groups.map { |ug| ug.name.downcase }
+ group_name = ordered_group_names.detect { |gn| user_group_names.include?(gn) }
+
+ to_notify[group_name] << user.id
+ end
+ already_covered_ids.concat(grouped[:already_participating])
+
+ to_notify[:welcome_to_join] = to_notify[:welcome_to_join].concat(grouped[:welcome_to_join])
+ to_notify[:unreachable] = to_notify[:unreachable].concat(grouped[:unreachable])
+ end
+
+ def notify_creator_of_inaccessible_mentions(unreachable, welcome_to_join)
+ return if unreachable.empty? && welcome_to_join.empty?
+
+ ChatPublisher.publish_inaccessible_mentions(
+ @user.id,
+ @chat_message,
+ unreachable,
+ welcome_to_join,
+ )
+ end
+
+ # Filters out users from global, here, group, and direct mentions that are
+ # ignoring or muting the creator of the message, so they will not receive
+ # a notification via the ChatNotifyMentioned job and are not prompted for
+ # invitation by the creator.
+ #
+ # already_covered_ids and to_notify sometimes contain IDs and sometimes contain
+ # Users, hence the gymnastics to resolve the user_id
+ def filter_users_ignoring_or_muting_creator(to_notify, already_covered_ids)
+ user_ids_to_screen =
+ already_covered_ids
+ .map { |ac| user_id_resolver(ac) }
+ .concat(to_notify.values.flatten.map { |tn| user_id_resolver(tn) })
+ .uniq
+ screener = UserCommScreener.new(acting_user: @user, target_user_ids: user_ids_to_screen)
+ to_notify
+ .except(:unreachable)
+ .each do |key, users_or_ids|
+ to_notify[key] = users_or_ids.reject do |user_or_id|
+ screener.ignoring_or_muting_actor?(user_id_resolver(user_or_id))
+ end
+ end
+ already_covered_ids.reject! do |already_covered|
+ screener.ignoring_or_muting_actor?(user_id_resolver(already_covered))
+ end
+ end
+
+ def user_id_resolver(obj)
+ obj.is_a?(User) ? obj.id : obj
+ end
+
+ def notify_mentioned_users(to_notify, already_notified_user_ids: [])
+ Jobs.enqueue(
+ :chat_notify_mentioned,
+ {
+ chat_message_id: @chat_message.id,
+ to_notify_ids_map: to_notify.as_json,
+ already_notified_user_ids: already_notified_user_ids,
+ timestamp: @timestamp.iso8601(6),
+ },
+ )
+ end
+
+ def notify_watching_users(except: [])
+ Jobs.enqueue(
+ :chat_notify_watching,
+ {
+ chat_message_id: @chat_message.id,
+ except_user_ids: except,
+ timestamp: @timestamp.iso8601(6),
+ },
+ )
+ end
+end
diff --git a/plugins/chat/lib/chat_review_queue.rb b/plugins/chat/lib/chat_review_queue.rb
new file mode 100644
index 0000000000..4b0392e151
--- /dev/null
+++ b/plugins/chat/lib/chat_review_queue.rb
@@ -0,0 +1,208 @@
+# frozen_string_literal: true
+
+# Acceptable options:
+# - message: Used when the flag type is notify_user or notify_moderators and we have to create
+# a separate PM.
+# - is_warning: Staff can send warnings when using the notify_user flag.
+# - take_action: Automatically approves the created reviewable and deletes the chat message.
+# - queue_for_review: Adds a special reason to the reviwable score and creates the reviewable using
+# the force_review option.
+
+class Chat::ChatReviewQueue
+ def flag_message(chat_message, guardian, flag_type_id, opts = {})
+ result = { success: false, errors: [] }
+
+ is_notify_type =
+ ReviewableScore.types.slice(:notify_user, :notify_moderators).values.include?(flag_type_id)
+ is_dm = chat_message.chat_channel.direct_message_channel?
+
+ raise Discourse::InvalidParameters.new(:flag_type) if is_dm && is_notify_type
+
+ guardian.ensure_can_flag_chat_message!(chat_message)
+ guardian.ensure_can_flag_message_as!(chat_message, flag_type_id, opts)
+
+ existing_reviewable = Reviewable.includes(:reviewable_scores).find_by(target: chat_message)
+
+ if !can_flag_again?(existing_reviewable, chat_message, guardian.user, flag_type_id)
+ result[:errors] << I18n.t("chat.reviewables.message_already_handled")
+ return result
+ end
+
+ payload = { message_cooked: chat_message.cooked }
+
+ if opts[:message].present? && !is_dm && is_notify_type
+ creator = companion_pm_creator(chat_message, guardian.user, flag_type_id, opts)
+ post = creator.create
+
+ if creator.errors.present?
+ creator.errors.full_messages.each { |msg| result[:errors] << msg }
+ return result
+ end
+ elsif is_dm
+ transcript = find_or_create_transcript(chat_message, guardian.user, existing_reviewable)
+ payload[:transcript_topic_id] = transcript.topic_id if transcript
+ end
+
+ queued_for_review = !!ActiveRecord::Type::Boolean.new.deserialize(opts[:queue_for_review])
+
+ reviewable =
+ ReviewableChatMessage.needs_review!(
+ created_by: guardian.user,
+ target: chat_message,
+ reviewable_by_moderator: true,
+ potential_spam: flag_type_id == ReviewableScore.types[:spam],
+ payload: payload,
+ )
+ reviewable.update(target_created_by: chat_message.user)
+ score =
+ reviewable.add_score(
+ guardian.user,
+ flag_type_id,
+ meta_topic_id: post&.topic_id,
+ take_action: opts[:take_action],
+ reason: queued_for_review ? "chat_message_queued_by_staff" : nil,
+ force_review: queued_for_review,
+ )
+
+ if opts[:take_action]
+ reviewable.perform(guardian.user, :agree_and_delete)
+ ChatPublisher.publish_delete!(chat_message.chat_channel, chat_message)
+ else
+ enforce_auto_silence_threshold(reviewable)
+ ChatPublisher.publish_flag!(chat_message, guardian.user, reviewable, score)
+ end
+
+ result.tap do |r|
+ r[:success] = true
+ r[:reviewable] = reviewable
+ end
+ end
+
+ private
+
+ def enforce_auto_silence_threshold(reviewable)
+ auto_silence_duration = SiteSetting.chat_auto_silence_from_flags_duration
+ return if auto_silence_duration.zero?
+ return if reviewable.score <= ReviewableChatMessage.score_to_silence_user
+
+ user = reviewable.target_created_by
+ return unless user
+ return if user.silenced?
+
+ UserSilencer.silence(
+ user,
+ Discourse.system_user,
+ silenced_till: auto_silence_duration.minutes.from_now,
+ reason: I18n.t("chat.errors.auto_silence_from_flags"),
+ )
+ end
+
+ def companion_pm_creator(chat_message, flagger, flag_type_id, opts)
+ notifying_user = flag_type_id == ReviewableScore.types[:notify_user]
+
+ i18n_key = notifying_user ? "notify_user" : "notify_moderators"
+
+ title =
+ I18n.t(
+ "reviewable_score_types.#{i18n_key}.chat_pm_title",
+ channel_name: chat_message.chat_channel.title(flagger),
+ locale: SiteSetting.default_locale,
+ )
+
+ body =
+ I18n.t(
+ "reviewable_score_types.#{i18n_key}.chat_pm_body",
+ message: opts[:message],
+ link: chat_message.full_url,
+ locale: SiteSetting.default_locale,
+ )
+
+ create_args = {
+ archetype: Archetype.private_message,
+ title: title.truncate(SiteSetting.max_topic_title_length, separator: /\s/),
+ raw: body,
+ }
+
+ if notifying_user
+ create_args[:subtype] = TopicSubtype.notify_user
+ create_args[:target_usernames] = chat_message.user.username
+
+ create_args[:is_warning] = opts[:is_warning] if flagger.staff?
+ else
+ create_args[:subtype] = TopicSubtype.notify_moderators
+ create_args[:target_group_names] = [Group[:moderators].name]
+ end
+
+ PostCreator.new(flagger, create_args)
+ end
+
+ def find_or_create_transcript(chat_message, flagger, existing_reviewable)
+ previous_message_ids =
+ ChatMessage
+ .where(chat_channel: chat_message.chat_channel)
+ .where("id < ?", chat_message.id)
+ .order("created_at DESC")
+ .limit(10)
+ .pluck(:id)
+ .reverse
+
+ return if previous_message_ids.empty?
+
+ service =
+ ChatTranscriptService.new(
+ chat_message.chat_channel,
+ Discourse.system_user,
+ messages_or_ids: previous_message_ids,
+ )
+
+ title =
+ I18n.t(
+ "chat.reviewables.direct_messages.transcript_title",
+ channel_name: chat_message.chat_channel.title(flagger),
+ locale: SiteSetting.default_locale,
+ )
+
+ body =
+ I18n.t(
+ "chat.reviewables.direct_messages.transcript_body",
+ transcript: service.generate_markdown,
+ locale: SiteSetting.default_locale,
+ )
+
+ create_args = {
+ archetype: Archetype.private_message,
+ title: title.truncate(SiteSetting.max_topic_title_length, separator: /\s/),
+ raw: body,
+ subtype: TopicSubtype.notify_moderators,
+ target_group_names: [Group[:moderators].name],
+ }
+
+ PostCreator.new(Discourse.system_user, create_args).create
+ end
+
+ def can_flag_again?(reviewable, message, flagger, flag_type_id)
+ return true if reviewable.blank?
+
+ flagger_has_pending_flags =
+ reviewable.reviewable_scores.any? { |rs| rs.user == flagger && rs.pending? }
+
+ if !flagger_has_pending_flags && flag_type_id == ReviewableScore.types[:notify_moderators]
+ return true
+ end
+
+ flag_used =
+ reviewable.reviewable_scores.any? do |rs|
+ rs.reviewable_score_type == flag_type_id && rs.pending?
+ end
+ handled_recently =
+ !(
+ reviewable.pending? ||
+ reviewable.updated_at < SiteSetting.cooldown_hours_until_reflag.to_i.hours.ago
+ )
+
+ latest_revision = message.revisions.last
+ edited_since_last_review = latest_revision && latest_revision.updated_at > reviewable.updated_at
+
+ !flag_used && !flagger_has_pending_flags && (!handled_recently || edited_since_last_review)
+ end
+end
diff --git a/plugins/chat/lib/chat_seeder.rb b/plugins/chat/lib/chat_seeder.rb
new file mode 100644
index 0000000000..79d8dc23bd
--- /dev/null
+++ b/plugins/chat/lib/chat_seeder.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+class ChatSeeder
+ def execute(args = {})
+ return if !SiteSetting.needs_chat_seeded
+
+ begin
+ create_category_channel_from(SiteSetting.staff_category_id)
+ create_category_channel_from(SiteSetting.general_category_id)
+ rescue => error
+ Rails.logger.warn("Error seeding chat category - #{error.inspect}")
+ ensure
+ SiteSetting.needs_chat_seeded = false
+ end
+ end
+
+ def create_category_channel_from(category_id)
+ category = Category.find_by(id: category_id)
+ return if category.nil?
+
+ chat_channel = category.create_chat_channel!(auto_join_users: true, name: category.name)
+ category.custom_fields[Chat::HAS_CHAT_ENABLED] = true
+ category.save!
+
+ Chat::ChatChannelMembershipManager.new(chat_channel).enforce_automatic_channel_memberships
+ chat_channel
+ end
+end
diff --git a/plugins/chat/lib/chat_statistics.rb b/plugins/chat/lib/chat_statistics.rb
new file mode 100644
index 0000000000..ab79fcf111
--- /dev/null
+++ b/plugins/chat/lib/chat_statistics.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+class Chat::Statistics
+ def self.about_messages
+ {
+ :last_day => ChatMessage.where("created_at > ?", 1.days.ago).count,
+ "7_days" => ChatMessage.where("created_at > ?", 7.days.ago).count,
+ "30_days" => ChatMessage.where("created_at > ?", 30.days.ago).count,
+ :previous_30_days =>
+ ChatMessage.where("created_at BETWEEN ? AND ?", 60.days.ago, 30.days.ago).count,
+ :count => ChatMessage.count,
+ }
+ end
+
+ def self.about_channels
+ {
+ :last_day => ChatChannel.where(status: :open).where("created_at > ?", 1.days.ago).count,
+ "7_days" => ChatChannel.where(status: :open).where("created_at > ?", 7.days.ago).count,
+ "30_days" => ChatChannel.where(status: :open).where("created_at > ?", 30.days.ago).count,
+ :previous_30_days =>
+ ChatChannel
+ .where(status: :open)
+ .where("created_at BETWEEN ? AND ?", 60.days.ago, 30.days.ago)
+ .count,
+ :count => ChatChannel.where(status: :open).count,
+ }
+ end
+
+ def self.about_users
+ {
+ :last_day => ChatMessage.where("created_at > ?", 1.days.ago).distinct.count(:user_id),
+ "7_days" => ChatMessage.where("created_at > ?", 7.days.ago).distinct.count(:user_id),
+ "30_days" => ChatMessage.where("created_at > ?", 30.days.ago).distinct.count(:user_id),
+ :previous_30_days =>
+ ChatMessage
+ .where("created_at BETWEEN ? AND ?", 60.days.ago, 30.days.ago)
+ .distinct
+ .count(:user_id),
+ :count => ChatMessage.distinct.count(:user_id),
+ }
+ end
+
+ def self.monthly
+ start_of_month = Time.zone.now.beginning_of_month
+ {
+ messages: ChatMessage.where("created_at > ?", start_of_month).count,
+ channels: ChatChannel.where(status: :open).where("created_at > ?", start_of_month).count,
+ users: ChatMessage.where("created_at > ?", start_of_month).distinct.count(:user_id),
+ }
+ end
+end
diff --git a/plugins/chat/lib/chat_transcript_service.rb b/plugins/chat/lib/chat_transcript_service.rb
new file mode 100644
index 0000000000..6326494cdd
--- /dev/null
+++ b/plugins/chat/lib/chat_transcript_service.rb
@@ -0,0 +1,177 @@
+# frozen_string_literal: true
+
+##
+# Used to generate BBCode [chat] tags for the message IDs provided.
+#
+# If there is > 1 message then the channel name will be shown at
+# the top of the first message, and subsequent messages will have
+# the chained attribute, which will affect how they are displayed
+# in the UI.
+#
+# Subsequent messages from the same user will be put into the same
+# tag. Each new user in the chain of messages will have a new [chat]
+# tag created.
+#
+# A single message will have the channel name displayed to the right
+# of the username and datetime of the message.
+class ChatTranscriptService
+ CHAINED_ATTR = "chained=\"true\""
+ MULTIQUOTE_ATTR = "multiQuote=\"true\""
+ NO_LINK_ATTR = "noLink=\"true\""
+
+ class ChatTranscriptBBCode
+ attr_reader :channel, :multiquote, :chained, :no_link, :include_reactions
+
+ def initialize(
+ channel: nil,
+ acting_user: nil,
+ multiquote: false,
+ chained: false,
+ no_link: false,
+ include_reactions: false
+ )
+ @channel = channel
+ @acting_user = acting_user
+ @multiquote = multiquote
+ @chained = chained
+ @no_link = no_link
+ @include_reactions = include_reactions
+ @message_data = []
+ end
+
+ def add(message:, reactions: nil)
+ @message_data << { message: message, reactions: reactions }
+ end
+
+ def render
+ attrs = [quote_attr(@message_data.first[:message])]
+
+ if channel
+ attrs << channel_attr
+ attrs << channel_id_attr
+ end
+
+ attrs << MULTIQUOTE_ATTR if multiquote
+ attrs << CHAINED_ATTR if chained
+ attrs << NO_LINK_ATTR if no_link
+ attrs << reactions_attr if include_reactions
+
+ <<~MARKDOWN
+ [chat #{attrs.compact.join(" ")}]
+ #{@message_data.map { |msg| msg[:message].to_markdown }.join("\n\n")}
+ [/chat]
+ MARKDOWN
+ end
+
+ private
+
+ def reactions_attr
+ reaction_data =
+ @message_data.reduce([]) do |array, msg_data|
+ if msg_data[:reactions].any?
+ array << msg_data[:reactions].map { |react| "#{react.emoji}:#{react.usernames}" }
+ end
+ array
+ end
+ return if reaction_data.empty?
+ "reactions=\"#{reaction_data.join(";")}\""
+ end
+
+ def quote_attr(message)
+ "quote=\"#{message.user.username};#{message.id};#{message.created_at.iso8601}\""
+ end
+
+ def channel_attr
+ "channel=\"#{channel.title(@acting_user)}\""
+ end
+
+ def channel_id_attr
+ "channelId=\"#{channel.id}\""
+ end
+ end
+
+ def initialize(channel, acting_user, messages_or_ids: [], opts: {})
+ @channel = channel
+ @acting_user = acting_user
+
+ if messages_or_ids.all? { |m| m.is_a?(Numeric) }
+ @message_ids = messages_or_ids
+ else
+ @messages = messages_or_ids
+ end
+ @opts = opts
+ end
+
+ def generate_markdown
+ previous_message = nil
+ rendered_markdown = []
+ all_messages_same_user = messages.count(:user_id) == 1
+ open_bbcode_tag =
+ ChatTranscriptBBCode.new(
+ channel: @channel,
+ acting_user: @acting_user,
+ multiquote: messages.length > 1,
+ chained: !all_messages_same_user,
+ no_link: @opts[:no_link],
+ include_reactions: @opts[:include_reactions],
+ )
+
+ messages.each.with_index do |message, idx|
+ if previous_message.present? && previous_message.user_id != message.user_id
+ rendered_markdown << open_bbcode_tag.render
+
+ open_bbcode_tag =
+ ChatTranscriptBBCode.new(
+ acting_user: @acting_user,
+ chained: !all_messages_same_user,
+ no_link: @opts[:no_link],
+ include_reactions: @opts[:include_reactions],
+ )
+ end
+
+ if @opts[:include_reactions]
+ open_bbcode_tag.add(message: message, reactions: reactions_for_message(message))
+ else
+ open_bbcode_tag.add(message: message)
+ end
+ previous_message = message
+ end
+
+ # tie off the last open bbcode + render
+ rendered_markdown << open_bbcode_tag.render
+ rendered_markdown.join("\n")
+ end
+
+ private
+
+ def messages
+ @messages ||=
+ ChatMessage
+ .includes(:user, chat_uploads: :upload)
+ .where(id: @message_ids, chat_channel_id: @channel.id)
+ .order(:created_at)
+ end
+
+ ##
+ # Queries reactions and returns them in this format
+ #
+ # emoji | usernames | chat_message_id
+ # ----------------------------------------
+ # +1 | foo,bar,baz | 102
+ # heart | foo | 102
+ # sob | bar,baz | 103
+ def reactions
+ @reactions ||= DB.query(<<~SQL, @messages.map(&:id))
+ SELECT emoji, STRING_AGG(DISTINCT users.username, ',') AS usernames, chat_message_id
+ FROM chat_message_reactions
+ INNER JOIN users on users.id = chat_message_reactions.user_id
+ WHERE chat_message_id IN (?)
+ GROUP BY emoji, chat_message_id
+ ORDER BY chat_message_id, emoji
+ SQL
+ end
+
+ def reactions_for_message(message)
+ reactions.select { |react| react.chat_message_id == message.id }
+ end
+end
diff --git a/plugins/chat/lib/direct_message_channel_creator.rb b/plugins/chat/lib/direct_message_channel_creator.rb
new file mode 100644
index 0000000000..06315b8a47
--- /dev/null
+++ b/plugins/chat/lib/direct_message_channel_creator.rb
@@ -0,0 +1,111 @@
+# frozen_string_literal: true
+
+module Chat::DirectMessageChannelCreator
+ class NotAllowed < StandardError
+ end
+
+ def self.create!(acting_user:, target_users:)
+ Guardian.new(acting_user).ensure_can_create_direct_message!
+ target_users.uniq!
+ direct_message = DirectMessage.for_user_ids(target_users.map(&:id))
+ if direct_message
+ chat_channel = ChatChannel.find_by!(chatable: direct_message)
+ else
+ ensure_actor_can_communicate!(acting_user, target_users)
+ direct_message = DirectMessage.create!(user_ids: target_users.map(&:id))
+ chat_channel = direct_message.create_chat_channel!
+ end
+
+ update_memberships(acting_user, target_users, chat_channel.id)
+ ChatPublisher.publish_new_channel(chat_channel, target_users)
+
+ chat_channel
+ end
+
+ private
+
+ def self.update_memberships(acting_user, target_users, chat_channel_id)
+ sql_params = {
+ acting_user_id: acting_user.id,
+ user_ids: target_users.map(&:id),
+ chat_channel_id: chat_channel_id,
+ always_notification_level: UserChatChannelMembership::NOTIFICATION_LEVELS[:always],
+ }
+
+ DB.exec(<<~SQL, sql_params)
+ INSERT INTO user_chat_channel_memberships(
+ user_id,
+ chat_channel_id,
+ muted,
+ following,
+ desktop_notification_level,
+ mobile_notification_level,
+ created_at,
+ updated_at
+ )
+ VALUES(
+ unnest(array[:user_ids]),
+ :chat_channel_id,
+ false,
+ false,
+ :always_notification_level,
+ :always_notification_level,
+ NOW(),
+ NOW()
+ )
+ ON CONFLICT (user_id, chat_channel_id) DO NOTHING;
+
+ UPDATE user_chat_channel_memberships
+ SET following = true
+ WHERE user_id = :acting_user_id AND chat_channel_id = :chat_channel_id;
+ SQL
+ end
+
+ def self.ensure_actor_can_communicate!(acting_user, target_users)
+ # We never want to prevent the actor from communicating with themself.
+ target_users = target_users.reject { |user| user.id == acting_user.id }
+
+ screener =
+ UserCommScreener.new(acting_user: acting_user, target_user_ids: target_users.map(&:id))
+
+ # People blocking the actor.
+ screener.preventing_actor_communication.each do |user_id|
+ raise NotAllowed.new(
+ I18n.t(
+ "chat.errors.not_accepting_dms",
+ username: target_users.find { |user| user.id == user_id }.username,
+ ),
+ )
+ end
+
+ # The actor cannot start DMs with people if they are not allowing anyone
+ # to start DMs with them, that's no fair!
+ if screener.actor_disallowing_all_pms?
+ raise NotAllowed.new(I18n.t("chat.errors.actor_disallowed_dms"))
+ end
+
+ # People the actor is blocking.
+ target_users.each do |target_user|
+ if screener.actor_disallowing_pms?(target_user.id)
+ raise NotAllowed.new(
+ I18n.t(
+ "chat.errors.actor_preventing_target_user_from_dm",
+ username: target_user.username,
+ ),
+ )
+ end
+
+ if screener.actor_ignoring?(target_user.id)
+ raise NotAllowed.new(
+ I18n.t("chat.errors.actor_ignoring_target_user", username: target_user.username),
+ )
+ end
+
+ if screener.actor_muting?(target_user.id)
+ raise NotAllowed.new(
+ I18n.t("chat.errors.actor_muting_target_user", username: target_user.username),
+ )
+ end
+ end
+ end
+end
diff --git a/plugins/chat/lib/discourse_dev/direct_channel.rb b/plugins/chat/lib/discourse_dev/direct_channel.rb
new file mode 100644
index 0000000000..4ee6e835fe
--- /dev/null
+++ b/plugins/chat/lib/discourse_dev/direct_channel.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require "discourse_dev/record"
+require "faker"
+
+module DiscourseDev
+ class DirectChannel < Record
+ def initialize
+ super(::DirectMessage, 5)
+ end
+
+ def data
+ if Faker::Boolean.boolean(true_ratio: 0.5)
+ admin_username =
+ begin
+ DiscourseDev::Config.new.config[:admin][:username]
+ rescue StandardError
+ nil
+ end
+ admin_user = ::User.find_by(username: admin_username) if admin_username
+ end
+
+ [User.new.create!, admin_user || User.new.create!]
+ end
+
+ def create!
+ users = data
+ Chat::DirectMessageChannelCreator.create!(acting_user: users[0], target_users: users)
+ end
+ end
+end
diff --git a/plugins/chat/lib/discourse_dev/message.rb b/plugins/chat/lib/discourse_dev/message.rb
new file mode 100644
index 0000000000..6cd72225a1
--- /dev/null
+++ b/plugins/chat/lib/discourse_dev/message.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require "discourse_dev/record"
+require "faker"
+
+module DiscourseDev
+ class Message < Record
+ def initialize
+ super(::ChatMessage, 200)
+ end
+
+ def data
+ if Faker::Boolean.boolean(true_ratio: 0.5)
+ channel = ::ChatChannel.where(chatable_type: "DirectMessage").order("RANDOM()").first
+ channel.user_chat_channel_memberships.update_all(following: true)
+ user = channel.chatable.users.order("RANDOM()").first
+ else
+ membership = ::UserChatChannelMembership.order("RANDOM()").first
+ channel = membership.chat_channel
+ user = membership.user
+ end
+
+ { user: user, content: Faker::Lorem.paragraph, chat_channel: channel }
+ end
+
+ def create!
+ Chat::ChatMessageCreator.create(data)
+ end
+ end
+end
diff --git a/plugins/chat/lib/discourse_dev/public_channel.rb b/plugins/chat/lib/discourse_dev/public_channel.rb
new file mode 100644
index 0000000000..cb9c672caa
--- /dev/null
+++ b/plugins/chat/lib/discourse_dev/public_channel.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require "discourse_dev/record"
+require "faker"
+
+module DiscourseDev
+ class PublicChannel < Record
+ def initialize
+ super(::CategoryChannel, 5)
+ end
+
+ def data
+ chatable = Category.random
+
+ {
+ chatable: chatable,
+ description: Faker::Lorem.paragraph,
+ user_count: 1,
+ name: Faker::Company.name,
+ created_at: Faker::Time.between(from: DiscourseDev.config.start_date, to: DateTime.now),
+ }
+ end
+
+ def create!
+ super do |channel|
+ Faker::Number
+ .between(from: 5, to: 10)
+ .times do
+ if Faker::Boolean.boolean(true_ratio: 0.5)
+ admin_username =
+ begin
+ DiscourseDev::Config.new.config[:admin][:username]
+ rescue StandardError
+ nil
+ end
+ admin_user = ::User.find_by(username: admin_username) if admin_username
+ end
+
+ Chat::ChatChannelMembershipManager.new(channel).follow(admin_user || User.new.create!)
+ end
+ end
+ end
+ end
+end
diff --git a/plugins/chat/lib/duplicate_message_validator.rb b/plugins/chat/lib/duplicate_message_validator.rb
new file mode 100644
index 0000000000..c66420f9d7
--- /dev/null
+++ b/plugins/chat/lib/duplicate_message_validator.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+class Chat::DuplicateMessageValidator
+ attr_reader :chat_message
+
+ def initialize(chat_message)
+ @chat_message = chat_message
+ end
+
+ def validate
+ return if SiteSetting.chat_duplicate_message_sensitivity.zero?
+ matrix =
+ Chat::DuplicateMessageValidator.sensitivity_matrix(
+ SiteSetting.chat_duplicate_message_sensitivity,
+ )
+
+ # Check if the length of the message is too short to check for a duplicate message
+ return if chat_message.message.length < matrix[:min_message_length]
+
+ # Check if there are enough users in the channel to check for a duplicate message
+ return if (chat_message.chat_channel.user_count || 0) < matrix[:min_user_count]
+
+ # Check if the same duplicate message has been posted in the last N seconds by any user
+ if !chat_message
+ .chat_channel
+ .chat_messages
+ .where("created_at > ?", matrix[:min_past_seconds].seconds.ago)
+ .where(message: chat_message.message)
+ .exists?
+ return
+ end
+
+ chat_message.errors.add(:base, I18n.t("chat.errors.duplicate_message"))
+ end
+
+ def self.sensitivity_matrix(sensitivity)
+ {
+ # 0.1 sensitivity = 100 users and 1.0 sensitivity = 5 users.
+ min_user_count: (-1.0 * 105.5 * sensitivity + 110.55).to_i,
+ # 0.1 sensitivity = 30 chars and 1.0 sensitivity = 10 chars.
+ min_message_length: (-1.0 * 22.2 * sensitivity + 32.22).to_i,
+ # 0.1 sensitivity = 10 seconds and 1.0 sensitivity = 60 seconds.
+ min_past_seconds: (55.55 * sensitivity + 4.5).to_i,
+ }
+ end
+end
diff --git a/plugins/chat/lib/email_controller_helper/chat_summary_unsubscriber.rb b/plugins/chat/lib/email_controller_helper/chat_summary_unsubscriber.rb
new file mode 100644
index 0000000000..ab4b06a757
--- /dev/null
+++ b/plugins/chat/lib/email_controller_helper/chat_summary_unsubscriber.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module EmailControllerHelper
+ class ChatSummaryUnsubscriber < BaseEmailUnsubscriber
+ def prepare_unsubscribe_options(controller)
+ super(controller)
+
+ chat_email_frequencies =
+ UserOption.chat_email_frequencies.map do |(freq, _)|
+ [I18n.t("unsubscribe.chat_summary.#{freq}"), freq]
+ end
+
+ controller.instance_variable_set(:@chat_email_frequencies, chat_email_frequencies)
+ controller.instance_variable_set(
+ :@current_chat_email_frequency,
+ key_owner.user_option.chat_email_frequency,
+ )
+ end
+
+ def unsubscribe(params)
+ updated = super(params)
+
+ if params[:chat_email_frequency]
+ key_owner.user_option.update!(chat_email_frequency: params[:chat_email_frequency])
+ updated = true
+ end
+
+ updated
+ end
+ end
+end
diff --git a/plugins/chat/lib/extensions/category_extension.rb b/plugins/chat/lib/extensions/category_extension.rb
new file mode 100644
index 0000000000..a424ac3810
--- /dev/null
+++ b/plugins/chat/lib/extensions/category_extension.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Chat::CategoryExtension
+ extend ActiveSupport::Concern
+
+ include Chatable
+
+ prepended { has_one :category_channel, as: :chatable }
+
+ def cannot_delete_reason
+ return I18n.t("category.cannot_delete.has_chat_channels") if category_channel
+ super
+ end
+
+ def deletable_for_chat?
+ return true if !category_channel
+ category_channel.chat_messages_empty?
+ end
+end
diff --git a/plugins/chat/lib/extensions/user_email_extension.rb b/plugins/chat/lib/extensions/user_email_extension.rb
new file mode 100644
index 0000000000..6742dccbe3
--- /dev/null
+++ b/plugins/chat/lib/extensions/user_email_extension.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Chat::UserEmailExtension
+ def execute(args)
+ super(args)
+
+ if args[:type] == "chat_summary" && args[:memberships_to_update_data].present?
+ args[:memberships_to_update_data].to_a.each do |membership_id, max_unread_mention_id|
+ UserChatChannelMembership.find_by(user: args[:user_id], id: membership_id.to_i)&.update(
+ last_unread_mention_when_emailed_id: max_unread_mention_id.to_i,
+ )
+ end
+ end
+ end
+end
diff --git a/plugins/chat/lib/extensions/user_extension.rb b/plugins/chat/lib/extensions/user_extension.rb
new file mode 100644
index 0000000000..b4c041d4d8
--- /dev/null
+++ b/plugins/chat/lib/extensions/user_extension.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Chat::UserExtension
+ extend ActiveSupport::Concern
+
+ prepended do
+ has_many :user_chat_channel_memberships, dependent: :destroy
+ has_many :chat_message_reactions, dependent: :destroy
+ has_many :chat_mentions
+ end
+end
diff --git a/plugins/chat/lib/extensions/user_notifications_extension.rb b/plugins/chat/lib/extensions/user_notifications_extension.rb
new file mode 100644
index 0000000000..6dd71b609c
--- /dev/null
+++ b/plugins/chat/lib/extensions/user_notifications_extension.rb
@@ -0,0 +1,122 @@
+# frozen_string_literal: true
+
+module Chat::UserNotificationsExtension
+ def chat_summary(user, opts)
+ guardian = Guardian.new(user)
+ return unless guardian.can_chat?(user)
+
+ @messages =
+ ChatMessage
+ .joins(:user, :chat_channel)
+ .where.not(user: user)
+ .where("chat_messages.created_at > ?", 1.week.ago)
+ .joins("LEFT OUTER JOIN chat_mentions cm ON cm.chat_message_id = chat_messages.id")
+ .joins(
+ "INNER JOIN user_chat_channel_memberships uccm ON uccm.chat_channel_id = chat_channels.id",
+ )
+ .where(<<~SQL, user_id: user.id)
+ uccm.user_id = :user_id AND
+ (uccm.last_read_message_id IS NULL OR chat_messages.id > uccm.last_read_message_id) AND
+ (uccm.last_unread_mention_when_emailed_id IS NULL OR chat_messages.id > uccm.last_unread_mention_when_emailed_id) AND
+ (
+ (cm.user_id = :user_id AND uccm.following IS true AND chat_channels.chatable_type = 'Category') OR
+ (chat_channels.chatable_type = 'DirectMessage')
+ )
+ SQL
+ .to_a
+
+ return if @messages.empty?
+ @grouped_messages = @messages.group_by { |message| message.chat_channel }
+ @grouped_messages =
+ @grouped_messages.select { |channel, _| guardian.can_see_chat_channel?(channel) }
+ return if @grouped_messages.empty?
+
+ @grouped_messages.each do |chat_channel, messages|
+ @grouped_messages[chat_channel] = messages.sort_by(&:created_at)
+ end
+ @user = user
+ @user_tz = UserOption.user_tzinfo(user.id)
+ @display_usernames = SiteSetting.prioritize_username_in_ux || !SiteSetting.enable_names
+
+ build_summary_for(user)
+ @preferences_path = "#{Discourse.base_url}/my/preferences/chat"
+
+ # TODO(roman): Remove after the 2.9 release
+ add_unsubscribe_link = UnsubscribeKey.respond_to?(:get_unsubscribe_strategy_for)
+
+ if add_unsubscribe_link
+ unsubscribe_key = UnsubscribeKey.create_key_for(@user, "chat_summary")
+ @unsubscribe_link = "#{Discourse.base_url}/email/unsubscribe/#{unsubscribe_key}"
+ opts[:unsubscribe_url] = @unsubscribe_link
+ end
+
+ opts = {
+ from_alias: I18n.t("user_notifications.chat_summary.from", site_name: Email.site_title),
+ subject: summary_subject(user, @grouped_messages),
+ add_unsubscribe_link: add_unsubscribe_link,
+ }
+
+ build_email(user.email, opts)
+ end
+
+ def summary_subject(user, grouped_messages)
+ channels = grouped_messages.keys
+ grouped_channels = channels.partition { |c| !c.direct_message_channel? }
+ non_dm_channels = grouped_channels.first
+ dm_users = grouped_channels.last.flat_map { |c| grouped_messages[c].map(&:user) }.uniq
+
+ total_count_for_subject = non_dm_channels.size + dm_users.size
+ first_message_from = non_dm_channels.pop
+ if first_message_from
+ first_message_title = first_message_from.title(user)
+ subject_key = "chat_channel"
+ else
+ subject_key = "direct_message"
+ first_message_from = dm_users.pop
+ first_message_title = first_message_from.username
+ end
+
+ subject_opts = {
+ email_prefix: @email_prefix,
+ count: total_count_for_subject,
+ message_title: first_message_title,
+ others:
+ other_channels_text(
+ user,
+ total_count_for_subject,
+ first_message_from,
+ non_dm_channels,
+ dm_users,
+ ),
+ }
+
+ I18n.t(with_subject_prefix(subject_key), **subject_opts)
+ end
+
+ def with_subject_prefix(key)
+ "user_notifications.chat_summary.subject.#{key}"
+ end
+
+ def other_channels_text(
+ user,
+ total_count,
+ first_message_from,
+ other_non_dm_channels,
+ other_dm_users
+ )
+ return if total_count <= 1
+ return I18n.t(with_subject_prefix("others"), count: total_count - 1) if total_count > 2
+
+ if other_non_dm_channels.empty?
+ second_message_from = other_dm_users.first
+ second_message_title = second_message_from.username
+ else
+ second_message_from = other_non_dm_channels.first
+ second_message_title = second_message_from.title(user)
+ end
+
+ return second_message_title if first_message_from.class == second_message_from.class
+
+ I18n.t(with_subject_prefix("other_direct_message"), message_title: second_message_title)
+ end
+end
diff --git a/plugins/chat/lib/extensions/user_option_extension.rb b/plugins/chat/lib/extensions/user_option_extension.rb
new file mode 100644
index 0000000000..ae2993a216
--- /dev/null
+++ b/plugins/chat/lib/extensions/user_option_extension.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Chat::UserOptionExtension
+ # TODO: remove last_emailed_for_chat and chat_isolated in 2023
+ def self.prepended(base)
+ if base.ignored_columns
+ base.ignored_columns = base.ignored_columns + %i[last_emailed_for_chat chat_isolated]
+ else
+ base.ignored_columns = %i[last_emailed_for_chat chat_isolated]
+ end
+
+ def base.chat_email_frequencies
+ @chat_email_frequencies ||= { never: 0, when_away: 1 }
+ end
+
+ base.enum :chat_email_frequency, base.chat_email_frequencies, prefix: "send_chat_email"
+ end
+end
diff --git a/plugins/chat/lib/guardian_extensions.rb b/plugins/chat/lib/guardian_extensions.rb
new file mode 100644
index 0000000000..cc04ac8629
--- /dev/null
+++ b/plugins/chat/lib/guardian_extensions.rb
@@ -0,0 +1,182 @@
+# frozen_string_literal: true
+
+module Chat::GuardianExtensions
+ def can_moderate_chat?(chatable)
+ case chatable.class.name
+ when "Category"
+ is_staff? || is_category_group_moderator?(chatable)
+ else
+ is_staff?
+ end
+ end
+
+ def can_chat?(user)
+ return false unless user
+
+ user.staff? || user.in_any_groups?(Chat.allowed_group_ids)
+ end
+
+ def can_create_chat_message?
+ !SpamRule::AutoSilence.prevent_posting?(@user)
+ end
+
+ def can_create_direct_message?
+ is_staff? || @user.in_any_groups?(SiteSetting.direct_message_enabled_groups_map)
+ end
+
+ def hidden_tag_names
+ @hidden_tag_names ||= DiscourseTagging.hidden_tag_names(self)
+ end
+
+ def can_create_chat_channel?
+ is_staff?
+ end
+
+ def can_delete_chat_channel?
+ is_staff?
+ end
+
+ # Channel status intentionally has no bearing on whether the channel
+ # name and description can be edited.
+ def can_edit_chat_channel?
+ is_staff?
+ end
+
+ def can_move_chat_messages?(channel)
+ can_moderate_chat?(channel.chatable)
+ end
+
+ def can_create_channel_message?(chat_channel)
+ valid_statuses = is_staff? ? %w[open closed] : ["open"]
+ valid_statuses.include?(chat_channel.status)
+ end
+
+ # This is intentionally identical to can_create_channel_message, we
+ # may want to have different conditions here in future.
+ def can_modify_channel_message?(chat_channel)
+ return chat_channel.open? || chat_channel.closed? if is_staff?
+ chat_channel.open?
+ end
+
+ def can_change_channel_status?(chat_channel, target_status)
+ return false if chat_channel.status.to_sym == target_status.to_sym
+ return false if !is_staff?
+
+ case target_status
+ when :closed
+ chat_channel.open?
+ when :open
+ chat_channel.closed?
+ when :archived
+ chat_channel.read_only?
+ when :read_only
+ chat_channel.closed? || chat_channel.open?
+ else
+ false
+ end
+ end
+
+ def can_rebake_chat_message?(message)
+ return false if !can_modify_channel_message?(message.chat_channel)
+ is_staff? || @user.has_trust_level?(TrustLevel[4])
+ end
+
+ def can_see_chat_channel?(chat_channel)
+ return false unless chat_channel.chatable
+
+ if chat_channel.direct_message_channel?
+ chat_channel.chatable.user_can_access?(@user)
+ elsif chat_channel.category_channel?
+ can_see_category?(chat_channel.chatable)
+ else
+ true
+ end
+ end
+
+ def can_flag_chat_messages?
+ return false if @user.silenced?
+
+ @user.in_any_groups?(SiteSetting.chat_message_flag_allowed_groups_map)
+ end
+
+ def can_flag_in_chat_channel?(chat_channel)
+ return false if !can_modify_channel_message?(chat_channel)
+
+ can_see_chat_channel?(chat_channel)
+ end
+
+ def can_flag_chat_message?(chat_message)
+ return false if !authenticated? || !chat_message || chat_message.trashed? || !chat_message.user
+ return false if chat_message.user.staff? && !SiteSetting.allow_flagging_staff
+ return false if chat_message.user_id == @user.id
+
+ can_flag_chat_messages? && can_flag_in_chat_channel?(chat_message.chat_channel)
+ end
+
+ def can_flag_message_as?(chat_message, flag_type_id, opts)
+ return false if !is_staff? && (opts[:take_action] || opts[:queue_for_review])
+
+ if flag_type_id == ReviewableScore.types[:notify_user]
+ is_warning = ActiveRecord::Type::Boolean.new.deserialize(opts[:is_warning])
+
+ return false if is_warning && !is_staff?
+ end
+
+ true
+ end
+
+ def can_delete_chat?(message, chatable)
+ return false if @user.silenced?
+ return false if !can_modify_channel_message?(message.chat_channel)
+
+ if message.user_id == current_user.id
+ can_delete_own_chats?(chatable)
+ else
+ can_delete_other_chats?(chatable)
+ end
+ end
+
+ def can_delete_own_chats?(chatable)
+ return false if (SiteSetting.max_post_deletions_per_day < 1)
+ return true if can_moderate_chat?(chatable)
+
+ true
+ end
+
+ def can_delete_other_chats?(chatable)
+ return true if can_moderate_chat?(chatable)
+
+ false
+ end
+
+ def can_restore_chat?(message, chatable)
+ return false if !can_modify_channel_message?(message.chat_channel)
+
+ if message.user_id == current_user.id
+ case chatable
+ when Category
+ return can_see_category?(chatable)
+ when DirectMessage
+ return true
+ end
+ end
+
+ can_delete_other_chats?(chatable)
+ end
+
+ def can_restore_other_chats?(chatable)
+ can_moderate_chat?(chatable)
+ end
+
+ def can_edit_chat?(message)
+ message.user_id == @user.id && !@user.silenced?
+ end
+
+ def can_react?
+ can_create_chat_message?
+ end
+
+ def can_delete_category?(category)
+ super && category.deletable_for_chat?
+ end
+end
diff --git a/plugins/chat/lib/message_mover.rb b/plugins/chat/lib/message_mover.rb
new file mode 100644
index 0000000000..fc290f757c
--- /dev/null
+++ b/plugins/chat/lib/message_mover.rb
@@ -0,0 +1,175 @@
+# frozen_string_literal: true
+
+##
+# Used to move chat messages from a chat channel to some other
+# location.
+#
+# Channel -> Channel:
+# -------------------
+#
+# Messages are sometimes misplaced and must be moved to another channel. For
+# now we only support moving messages between public channels, handling the
+# permissions and membership around moving things in and out of DMs is a little
+# much for V1.
+#
+# The original messages will be deleted, and then similar to PostMover in core,
+# all of the references associated to a chat message (e.g. reactions, bookmarks,
+# notifications, revisions, mentions, uploads) will be updated to the new
+# message IDs via a moved_chat_messages temporary table.
+class Chat::MessageMover
+ class NoMessagesFound < StandardError
+ end
+ class InvalidChannel < StandardError
+ end
+
+ def initialize(acting_user:, source_channel:, message_ids:)
+ @source_channel = source_channel
+ @acting_user = acting_user
+ @source_message_ids = message_ids
+ @source_messages = find_messages(@source_message_ids, source_channel)
+ @ordered_source_message_ids = @source_messages.map(&:id)
+ end
+
+ def move_to_channel(destination_channel)
+ if !@source_channel.public_channel? || !destination_channel.public_channel?
+ raise InvalidChannel.new(I18n.t("chat.errors.message_move_invalid_channel"))
+ end
+
+ if @ordered_source_message_ids.empty?
+ raise NoMessagesFound.new(I18n.t("chat.errors.message_move_no_messages_found"))
+ end
+
+ moved_messages = nil
+
+ ChatMessage.transaction do
+ create_temp_table
+ moved_messages =
+ find_messages(
+ create_destination_messages_in_channel(destination_channel),
+ destination_channel,
+ )
+ bulk_insert_movement_metadata
+ update_references
+ delete_source_messages
+ end
+
+ add_moved_placeholder(destination_channel, moved_messages.first)
+ moved_messages
+ end
+
+ private
+
+ def find_messages(message_ids, channel)
+ ChatMessage.where(id: message_ids, chat_channel_id: channel.id).order("created_at ASC, id ASC")
+ end
+
+ def create_temp_table
+ DB.exec("DROP TABLE IF EXISTS moved_chat_messages") if Rails.env.test?
+
+ DB.exec <<~SQL
+ CREATE TEMPORARY TABLE moved_chat_messages (
+ old_chat_message_id INTEGER,
+ new_chat_message_id INTEGER
+ ) ON COMMIT DROP;
+
+ CREATE INDEX moved_chat_messages_old_chat_message_id ON moved_chat_messages(old_chat_message_id);
+ SQL
+ end
+
+ def bulk_insert_movement_metadata
+ values_sql = @movement_metadata.map { |mm| "(#{mm[:old_id]}, #{mm[:new_id]})" }.join(",\n")
+ DB.exec(
+ "INSERT INTO moved_chat_messages(old_chat_message_id, new_chat_message_id) VALUES #{values_sql}",
+ )
+ end
+
+ ##
+ # We purposefully omit in_reply_to_id when creating the messages in the
+ # new channel, because it could be pointing to a message that has not
+ # been moved.
+ def create_destination_messages_in_channel(destination_channel)
+ query_args = {
+ message_ids: @ordered_source_message_ids,
+ destination_channel_id: destination_channel.id,
+ }
+ moved_message_ids = DB.query_single(<<~SQL, query_args)
+ INSERT INTO chat_messages(
+ chat_channel_id, user_id, last_editor_id, message, cooked, cooked_version, created_at, updated_at
+ )
+ SELECT :destination_channel_id,
+ user_id,
+ last_editor_id,
+ message,
+ cooked,
+ cooked_version,
+ CLOCK_TIMESTAMP(),
+ CLOCK_TIMESTAMP()
+ FROM chat_messages
+ WHERE id IN (:message_ids)
+ RETURNING id
+ SQL
+
+ @movement_metadata =
+ moved_message_ids.map.with_index do |chat_message_id, idx|
+ { old_id: @ordered_source_message_ids[idx], new_id: chat_message_id }
+ end
+ moved_message_ids
+ end
+
+ def update_references
+ DB.exec(<<~SQL)
+ UPDATE chat_message_reactions cmr
+ SET chat_message_id = mm.new_chat_message_id
+ FROM moved_chat_messages mm
+ WHERE cmr.chat_message_id = mm.old_chat_message_id
+ SQL
+
+ DB.exec(<<~SQL)
+ UPDATE chat_uploads cu
+ SET chat_message_id = mm.new_chat_message_id
+ FROM moved_chat_messages mm
+ WHERE cu.chat_message_id = mm.old_chat_message_id
+ SQL
+
+ DB.exec(<<~SQL)
+ UPDATE chat_mentions cment
+ SET chat_message_id = mm.new_chat_message_id
+ FROM moved_chat_messages mm
+ WHERE cment.chat_message_id = mm.old_chat_message_id
+ SQL
+
+ DB.exec(<<~SQL)
+ UPDATE chat_message_revisions crev
+ SET chat_message_id = mm.new_chat_message_id
+ FROM moved_chat_messages mm
+ WHERE crev.chat_message_id = mm.old_chat_message_id
+ SQL
+
+ DB.exec(<<~SQL)
+ UPDATE chat_webhook_events cweb
+ SET chat_message_id = mm.new_chat_message_id
+ FROM moved_chat_messages mm
+ WHERE cweb.chat_message_id = mm.old_chat_message_id
+ SQL
+ end
+
+ def delete_source_messages
+ @source_messages.update_all(deleted_at: Time.zone.now, deleted_by_id: @acting_user.id)
+ ChatPublisher.publish_bulk_delete!(@source_channel, @source_message_ids)
+ end
+
+ def add_moved_placeholder(destination_channel, first_moved_message)
+ Chat::ChatMessageCreator.create(
+ chat_channel: @source_channel,
+ user: Discourse.system_user,
+ content:
+ I18n.t(
+ "chat.channel.messages_moved",
+ count: @source_message_ids.length,
+ acting_username: @acting_user.username,
+ channel_name: destination_channel.title(@acting_user),
+ first_moved_message_url: first_moved_message.url,
+ ),
+ )
+ end
+end
diff --git a/plugins/chat/lib/onebox/templates/discourse_chat.mustache b/plugins/chat/lib/onebox/templates/discourse_chat.mustache
new file mode 100644
index 0000000000..b0bcc8ef5e
--- /dev/null
+++ b/plugins/chat/lib/onebox/templates/discourse_chat.mustache
@@ -0,0 +1,58 @@
+{{^cooked}}
+
+{{/cooked}}
+
+{{#cooked}}
+
+
+
1
2test
")
+
+ expect(cooked).to eq("test
+ COOKED
+ end
+
+ it "supports fence rule with language support" do
+ cooked = ChatMessage.cook(<<~RAW)
+ ```ruby
+ Widget.triangulate(argument: "no u")
+ ```
+ RAW
+
+ expect(cooked).to eq(<<~COOKED.chomp)
+ something = test
+
+ COOKED
+ end
+
+ it "supports code rule" do
+ cooked = ChatMessage.cook(" something = test")
+
+ expect(cooked).to eq("Widget.triangulate(argument: "no u")
+
")
+ end
+
+ it "supports blockquote rule" do
+ cooked = ChatMessage.cook("> a quote")
+
+ expect(cooked).to eq("something = test\n\n
")
+ end
+
+ it "supports quote bbcode" do
+ topic = Fabricate(:topic, title: "Some quotable topic")
+ post = Fabricate(:post, topic: topic)
+ SiteSetting.external_system_avatars_enabled = false
+ avatar_src =
+ "//test.localhost#{User.system_avatar_template(post.user.username).gsub("{size}", "40")}"
+
+ cooked = ChatMessage.cook(<<~RAW)
+ [quote="#{post.user.username}, post:#{post.post_number}, topic:#{topic.id}"]
+ Mark me...this will go down in history.
+ [/quote]
+ RAW
+
+ expect(cooked).to eq(<<~COOKED.chomp)
+
+ COOKED
+ end
+
+ it "supports chat quote bbcode" do
+ chat_channel = Fabricate(:category_channel, name: "testchannel")
+ user = Fabricate(:user, username: "chatbbcodeuser")
+ user2 = Fabricate(:user, username: "otherbbcodeuser")
+ avatar_src =
+ "//test.localhost#{User.system_avatar_template(user.username).gsub("{size}", "40")}"
+ avatar_src2 =
+ "//test.localhost#{User.system_avatar_template(user2.username).gsub("{size}", "40")}"
+ msg1 =
+ Fabricate(
+ :chat_message,
+ chat_channel: chat_channel,
+ message: "this is the first message",
+ user: user,
+ )
+ msg2 =
+ Fabricate(
+ :chat_message,
+ chat_channel: chat_channel,
+ message: "and another cool one",
+ user: user2,
+ )
+ other_messages_to_quote = [msg1, msg2]
+ cooked =
+ ChatMessage.cook(
+ ChatTranscriptService.new(
+ chat_channel,
+ Fabricate(:user),
+ messages_or_ids: other_messages_to_quote.map(&:id),
+ ).generate_markdown,
+ )
+
+ expect(cooked).to eq(<<~COOKED.chomp)
+
+
test
+
+
+
+
+
+
+ Command
+ Description
+
+
+
+ git status
+ List all new or modified files
+ 
+
+ HTML
+ end
+
+ it "supports inline emoji" do
+ cooked = ChatMessage.cook(":D")
+ expect(cooked).to eq(<<~HTML.chomp)
+ 

@#{user.username}
")
+
+ expect(cooked).to eq("hello there
";
+ await publishToMessageBus(`/chat/11`, {
+ type: "processed",
+ chat_message: {
+ cooked,
+ id: 175,
+ },
+ });
+
+ assert.ok(
+ query(
+ ".chat-message-container[data-id='175'] .chat-message-text"
+ ).innerHTML.includes(cooked)
+ );
+ });
+
+ test("Code highlighting in a message", async function (assert) {
+ await visit("/chat/channel/11/another-category");
+ const messageContent = `Here's a message with code highlighting
+
+\`\`\`ruby
+Widget.triangulate(arg: "test")
+\`\`\``;
+ const composerInput = query(".chat-composer-input");
+ await fillIn(composerInput, messageContent);
+ await focus(composerInput);
+ await triggerKeyEvent(composerInput, "keydown", "Enter");
+
+ await publishToMessageBus("/chat/11", {
+ type: "sent",
+ stagedId: 1,
+ chat_message: {
+ id: 202,
+ cooked: `
`,
+ user: {
+ id: 1,
+ },
+ },
+ });
+
+ const messages = queryAll(".chat-message");
+ const lastMessage = messages[messages.length - 1];
+ assert.equal(
+ lastMessage.closest(".chat-message-container").dataset.id,
+ 202
+ );
+ assert.ok(
+ exists(
+ ".chat-message-container[data-id='202'] .chat-message-text code.lang-ruby.hljs"
+ ),
+ "chat message code block has been highlighted as ruby code"
+ );
+ });
+
+ test("Drafts are saved and reloaded", async function (assert) {
+ await visit("/chat/channel/11/another-category");
+ await fillIn(".chat-composer-input", "Hi people");
+
+ await visit("/chat/channel/75/@hawk");
+ assert.equal(query(".chat-composer-input").value.trim(), "");
+ await fillIn(".chat-composer-input", "What up what up");
+
+ await visit("/chat/channel/11/another-category");
+ assert.equal(query(".chat-composer-input").value.trim(), "Hi people");
+ await fillIn(".chat-composer-input", "");
+
+ await visit("/chat/channel/75/@hawk");
+ assert.equal(query(".chat-composer-input").value.trim(), "What up what up");
+
+ // Send a message
+ const composerTextarea = query(".chat-composer-input");
+ await focus(composerTextarea);
+ await triggerKeyEvent(composerTextarea, "keydown", "Enter");
+
+ assert.equal(query(".chat-composer-input").value.trim(), "");
+
+ // Navigate away and back to make sure input didn't re-fill
+ await visit("/chat/channel/11/another-category");
+ await visit("/chat/channel/75/@hawk");
+ assert.equal(query(".chat-composer-input").value.trim(), "");
+ });
+
+ test("Pressing escape cancels editing", async function (assert) {
+ await visit("/chat/channel/11/another-category");
+ await triggerEvent(".chat-message-container[data-id='174']", "mouseenter");
+
+ const dropdown = selectKit(".more-buttons");
+ await dropdown.expand();
+ await dropdown.selectRowByValue("edit");
+
+ assert.ok(exists(".chat-composer-message-details"));
+ await triggerKeyEvent(".chat-composer", "keydown", "Escape");
+
+ // chat-composer-message-details will be gone as no message is being edited
+ assert.notOk(exists(".chat-composer .chat-composer-message-details"));
+ });
+
+ test("Unread indicator increments for public channels when messages come in", async function (assert) {
+ await visit("/t/internationalization-localization/280");
+ assert.notOk(
+ exists(".header-dropdown-toggle.open-chat .chat-channel-unread-indicator")
+ );
+
+ await publishToMessageBus("/chat/9/new-messages", {
+ message_id: 201,
+ user_id: 2,
+ });
+
+ assert.ok(
+ exists(".header-dropdown-toggle.open-chat .chat-channel-unread-indicator")
+ );
+ });
+
+ test("Unread count increments for direct message channels when messages come in", async function (assert) {
+ await visit("/t/internationalization-localization/280");
+ assert.notOk(
+ exists(
+ ".header-dropdown-toggle.open-chat .chat-channel-unread-indicator.urgent .number"
+ )
+ );
+
+ await publishToMessageBus("/chat/75/new-messages", {
+ message_id: 201,
+ user_id: 2,
+ });
+ assert.ok(
+ exists(
+ ".header-dropdown-toggle.open-chat .chat-channel-unread-indicator.urgent .number"
+ )
+ );
+ assert.equal(
+ query(
+ ".header-dropdown-toggle.open-chat .chat-channel-unread-indicator.urgent .number"
+ ).innerText.trim(),
+ 1
+ );
+ });
+
+ test("Unread DM count overrides the public unread indicator", async function (assert) {
+ await visit("/t/internationalization-localization/280");
+ await publishToMessageBus("/chat/9/new-messages", {
+ message_id: 201,
+ user_id: 2,
+ });
+ await publishToMessageBus("/chat/75/new-messages", {
+ message_id: 202,
+ user_id: 2,
+ });
+ assert.ok(
+ exists(
+ ".header-dropdown-toggle.open-chat .chat-channel-unread-indicator.urgent .number"
+ )
+ );
+ assert.notOk(
+ exists(
+ ".header-dropdown-toggle.open-chat .chat-channel-unread-indicator:not(.urgent)"
+ )
+ );
+ });
+
+ test("Mentions in public channels show the unread urgent indicator", async function (assert) {
+ await visit("/t/internationalization-localization/280");
+ await publishToMessageBus("/chat/9/new-mentions", {
+ message_id: 201,
+ });
+ assert.ok(
+ exists(
+ ".header-dropdown-toggle.open-chat .chat-channel-unread-indicator.urgent .number"
+ )
+ );
+ assert.notOk(
+ exists(
+ ".header-dropdown-toggle.open-chat .chat-channel-unread-indicator:not(.urgent)"
+ )
+ );
+ });
+
+ test("message selection and live pane buttons for regular user", async function (assert) {
+ updateCurrentUser({ admin: false, moderator: false });
+ await visit("/chat/channel/11/another-category");
+
+ const firstMessage = query(".chat-message-container");
+ await triggerEvent(firstMessage, "mouseenter");
+ const dropdown = selectKit(
+ `.chat-message-actions-container[data-id="${firstMessage.dataset.id}"] .more-buttons`
+ );
+ await dropdown.expand();
+ await dropdown.selectRowByValue("selectMessage");
+
+ assert.ok(firstMessage.classList.contains("selecting-messages"));
+ assert.ok(exists("#chat-quote-btn"));
+ });
+
+ test("message selection is not present for regular user", async function (assert) {
+ updateCurrentUser({ admin: false, moderator: false });
+ await visit("/chat/channel/11/another-category");
+ assert.notOk(
+ exists(
+ ".chat-message-container .chat-message-actions-container .select-btn"
+ )
+ );
+ });
+
+ test("creating a new direct message channel works", async function (assert) {
+ await visit("/chat/channel/11/another-category");
+ await click(".open-draft-channel-page-btn");
+ await fillIn(".filter-usernames", "hawk");
+ await click("li.user[data-username='hawk']");
+
+ assert.notOk(
+ query(".join-channel-btn"),
+ "Join channel button is not present"
+ );
+ const enabledComposer = document.querySelector(".chat-composer-input");
+ assert.ok(!enabledComposer.disabled);
+ assert.equal(
+ enabledComposer.placeholder,
+ I18n.t("chat.placeholder_start_conversation", { usernames: "hawk" })
+ );
+ });
+
+ test("creating a new direct message channel from popup chat works", async function (assert) {
+ await visit("/t/internationalization-localization/280");
+ await click(".open-draft-channel-page-btn");
+ await fillIn(".filter-usernames", "hawk");
+ await click('.chat-user-avatar-container[data-user-card="hawk"]');
+ assert.ok(query(".selected-user").innerText, "hawk");
+ });
+
+ test("Reacting works with no existing reactions", async function (assert) {
+ await visit("/chat/channel/11/another-category");
+ const message = query(".chat-message-container");
+ await triggerEvent(message, "mouseenter");
+ assert.notOk(message.querySelector(".chat-message-reaction-list"));
+ await click(".chat-message-actions .react-btn");
+ await click(`.chat-emoji-picker .emoji[alt="grinning"]`);
+
+ assert.ok(message.querySelector(".chat-message-reaction-list"));
+ const reaction = message.querySelector(
+ ".chat-message-reaction-list .chat-message-reaction.reacted"
+ );
+ assert.ok(reaction);
+ assert.equal(reaction.querySelector(".count").innerText.trim(), 1);
+ });
+
+ test("Reacting works with existing reactions", async function (assert) {
+ await visit("/chat/channel/11/another-category");
+ const messages = queryAll(".chat-message-container");
+
+ // First 2 messages have no reactions; make sure the list isn't rendered
+ assert.notOk(messages[0].querySelector(".chat-message-reaction-list"));
+ assert.notOk(messages[1].querySelector(".chat-message-reaction-list"));
+
+ const lastMessage = messages[2];
+ assert.ok(lastMessage.querySelector(".chat-message-reaction-list"));
+ assert.equal(
+ lastMessage.querySelectorAll(".chat-message-reaction.reacted").length,
+ 2
+ );
+ assert.equal(
+ lastMessage.querySelectorAll(".chat-message-reaction:not(.reacted)")
+ .length,
+ 1
+ );
+
+ // React with a heart and make sure the count increments and class is added
+ const heartReaction = lastMessage.querySelector(
+ `.chat-message-reaction[data-emoji-name="heart"]`
+ );
+ assert.equal(heartReaction.innerText.trim(), "1");
+ await click(heartReaction);
+ assert.equal(heartReaction.innerText.trim(), "2");
+ assert.ok(heartReaction.classList.contains("reacted"));
+
+ await publishToMessageBus("/chat/11", {
+ action: "add",
+ user: { id: 1, username: "eviltrout" },
+ emoji: "heart",
+ type: "reaction",
+ chat_message_id: 176,
+ });
+
+ // Click again make sure count goes down
+ await click(heartReaction);
+ assert.equal(heartReaction.innerText.trim(), "1");
+ assert.notOk(heartReaction.classList.contains("reacted"));
+
+ // Message from another user coming in!
+ await publishToMessageBus("/chat/11", {
+ action: "add",
+ user: { id: 77, username: "rando" },
+ emoji: "sneezing_face",
+ type: "reaction",
+ chat_message_id: 176,
+ });
+ const sneezingFaceReaction = lastMessage.querySelector(
+ `.chat-message-reaction[data-emoji-name="sneezing_face"]`
+ );
+ assert.ok(sneezingFaceReaction);
+ assert.equal(sneezingFaceReaction.innerText.trim(), "1");
+ assert.notOk(sneezingFaceReaction.classList.contains("reacted"));
+ await click(sneezingFaceReaction);
+ assert.equal(sneezingFaceReaction.innerText.trim(), "2");
+ assert.ok(sneezingFaceReaction.classList.contains("reacted"));
+ });
+
+ test("Reacting and unreacting works on newly created chat messages", async function (assert) {
+ await visit("/chat/channel/11/another-category");
+ const composerInput = query(".chat-composer-input");
+ await fillIn(composerInput, "hellloooo");
+ await focus(composerInput);
+ await triggerKeyEvent(composerInput, "keydown", "Enter");
+
+ const messages = queryAll(".chat-message-container");
+ const lastMessage = messages[messages.length - 1];
+ await publishToMessageBus("/chat/11", {
+ type: "sent",
+ stagedId: 1,
+ chat_message: {
+ id: 202,
+ user: {
+ id: 1,
+ },
+ cooked: "Widget.triangulate(arg: "test")
+
This is a chat message.
", { + messageId: "2321", + username: "martin", + datetime: "2022-01-25T05:40:39Z", + timezone: "Australia/Brisbane", + }), + "renders the chat message with the required CSS classes and attributes" + ); + }); + + test("renders the channel name if provided with multiQuote", function (assert) { + assert.cookedChatTranscript( + `[chat quote="martin;2321;2022-01-25T05:40:39Z" channel="Cool Cats Club" channelId="1234" multiQuote="true"]\nThis is a chat message.\n[/chat]`, + { additionalOptions }, + generateTranscriptHTML("This is a chat message.
", { + messageId: "2321", + username: "martin", + datetime: "2022-01-25T05:40:39Z", + channel: "Cool Cats Club", + channelId: "1234", + multiQuote: true, + timezone: "Australia/Brisbane", + }), + "renders the chat transcript with the channel name included above the user and datetime" + ); + }); + + test("renders the channel name if provided without multiQuote", function (assert) { + assert.cookedChatTranscript( + `[chat quote="martin;2321;2022-01-25T05:40:39Z" channel="Cool Cats Club" channelId="1234"]\nThis is a chat message.\n[/chat]`, + { additionalOptions }, + generateTranscriptHTML("This is a chat message.
", { + messageId: "2321", + username: "martin", + datetime: "2022-01-25T05:40:39Z", + channel: "Cool Cats Club", + channelId: "1234", + timezone: "Australia/Brisbane", + }), + "renders the chat transcript with the channel name included next to the datetime" + ); + }); + + test("renders with the chained attribute for more compact quotes", function (assert) { + assert.cookedChatTranscript( + `[chat quote="martin;2321;2022-01-25T05:40:39Z" channel="Cool Cats Club" channelId="1234" multiQuote="true" chained="true"]\nThis is a chat message.\n[/chat]`, + { additionalOptions }, + generateTranscriptHTML("This is a chat message.
", { + messageId: "2321", + username: "martin", + datetime: "2022-01-25T05:40:39Z", + channel: "Cool Cats Club", + channelId: "1234", + multiQuote: true, + chained: true, + timezone: "Australia/Brisbane", + }), + "renders with the chained attribute" + ); + }); + + test("renders with the noLink attribute to remove the links to the individual messages from the datetimes", function (assert) { + assert.cookedChatTranscript( + `[chat quote="martin;2321;2022-01-25T05:40:39Z" channel="Cool Cats Club" channelId="1234" multiQuote="true" noLink="true"]\nThis is a chat message.\n[/chat]`, + { additionalOptions }, + generateTranscriptHTML("This is a chat message.
", { + messageId: "2321", + username: "martin", + datetime: "2022-01-25T05:40:39Z", + channel: "Cool Cats Club", + channelId: "1234", + multiQuote: true, + noLink: true, + timezone: "Australia/Brisbane", + }), + "renders with the noLink attribute" + ); + }); + + test("renders with the reactions attribute", function (assert) { + const reactionsAttr = "+1:martin;heart:martin,eviltrout"; + assert.cookedChatTranscript( + `[chat quote="martin;2321;2022-01-25T05:40:39Z" channel="Cool Cats Club" channelId="1234" reactions="${reactionsAttr}"]\nThis is a chat message.\n[/chat]`, + { additionalOptions }, + generateTranscriptHTML("This is a chat message.
", { + messageId: "2321", + username: "martin", + datetime: "2022-01-25T05:40:39Z", + channel: "Cool Cats Club", + channelId: "1234", + timezone: "Australia/Brisbane", + reactionsAttr, + reactions: [ + { emoji: "+1", usernames: ["martin"] }, + { emoji: "heart", usernames: ["martin", "eviltrout"] }, + ], + }), + "renders with the reaction data attribute and HTML" + ); + }); + + test("renders with minimal markdown rules inside the quote bbcode block, same as server-side chat messages", function (assert) { + assert.cookedChatTranscript( + `[chat quote="johnsmith;450;2021-04-25T05:40:39Z"] +[quote="martin, post:3, topic:6215"] +another cool reply +[/quote] +[/chat]`, + { additionalOptions }, + generateTranscriptHTML( + `[quote="martin, post:3, topic:6215"]
+another cool reply
+[/quote]
This does work with removed rules.
This ~~does work~~ with removed _rules_.
+* list item 1
`, + { + messageId: "2321", + username: "martin", + datetime: "2022-01-25T05:40:39Z", + timezone: "Australia/Brisbane", + } + ), + "renders correctly with some obvious rules excluded (list/strikethrough/emphasis)" + ); + + assert.cookedChatTranscript( + `[chat quote="martin;2321;2022-01-25T05:40:39Z"]\nhere is a message :P with category hashtag #test\n[/chat]`, + { additionalOptions }, + generateTranscriptHTML( + `here is a message
with category hashtag #test
here is a message :P with category hashtag #test
`, + { + messageId: "2321", + username: "martin", + datetime: "2022-01-25T05:40:39Z", + timezone: "Australia/Brisbane", + } + ), + "renders correctly with some obvious features excluded (category-hashtag, emojiShortcuts)" + ); + + assert.cookedChatTranscript( + `This ~~does work~~ with removed _rules_. + +* list item 1 + +here is a message :P with category hashtag #test + +[chat quote="martin;2321;2022-01-25T05:40:39Z"] +This ~~does work~~ with removed _rules_. + +* list item 1 + +here is a message :P with category hashtag #test +[/chat]`, + { additionalOptions }, + `This does work with removed rules.
here is a message
with category hashtag #test
This ~~does work~~ with removed _rules_.
+* list item 1
+here is a message :P with category hashtag #test
`, + { + messageId: "2321", + username: "martin", + datetime: "2022-01-25T05:40:39Z", + timezone: "Australia/Brisbane", + } + ), + "the rule changes do not apply outside the BBCode [chat] block" + ); + }); +}); + +acceptance( + "Discourse Chat | chat-transcript date decoration", + function (needs) { + let additionalOptions = buildAdditionalOptions(); + + needs.user({ + admin: false, + moderator: false, + username: "eviltrout", + id: 1, + can_chat: true, + has_chat_enabled: true, + timezone: "Australia/Brisbane", + }); + needs.settings({ + chat_enabled: true, + }); + + needs.pretender((server, helper) => { + server.get("/chat/chat_channels.json", () => + helper.response({ + public_channels: [], + direct_message_channels: [], + }) + ); + + const topicResponse = cloneJSON(topicFixtures["/t/280/1.json"]); + const firstPost = topicResponse.post_stream.posts[0]; + const postCooked = cookMarkdown( + `[chat quote="martin;2321;2022-01-25T05:40:39Z"]\nThis is a chat message.\n[/chat]`, + { additionalOptions } + ); + firstPost.cooked += postCooked; + + server.get("/t/280.json", () => helper.response(topicResponse)); + }); + + test("chat transcript datetimes are formatted into the link with decorateCookedElement", async function (assert) { + await visit("/t/-/280"); + + assert.strictEqual( + query(".chat-transcript-datetime span").innerText.trim(), + moment + .tz("2022-01-25T05:40:39Z", "Australia/Brisbane") + .format(I18n.t("dates.long_no_year")), + "it decorates the chat transcript datetime link with a formatted date" + ); + }); + } +); + +acceptance( + "Discourse Chat - chat-transcript - Composer Oneboxes ", + function (needs) { + let additionalOptions = buildAdditionalOptions(); + needs.user({ + admin: false, + moderator: false, + username: "eviltrout", + id: 1, + can_chat: true, + has_chat_enabled: true, + timezone: "Australia/Brisbane", + }); + needs.settings({ + chat_enabled: true, + enable_markdown_linkify: true, + max_oneboxes_per_post: 2, + }); + needs.pretender((server, helper) => { + server.get("/chat/chat_channels.json", () => + helper.response({ + public_channels: [], + direct_message_channels: [], + }) + ); + + const topicResponse = cloneJSON(topicFixtures["/t/280/1.json"]); + const firstPost = topicResponse.post_stream.posts[0]; + const postCooked = cookMarkdown( + `[chat quote="martin;2321;2022-01-25T05:40:39Z"]\nThis is a chat message.\n[/chat]`, + { additionalOptions } + ); + firstPost.cooked += postCooked; + + server.get("/t/280.json", () => helper.response(topicResponse)); + }); + + test("Preview should not error for oneboxes within [chat] bbcode", async function (assert) { + await visit("/t/internationalization-localization/280"); + await click("#topic-footer-buttons .btn.create"); + + await fillIn( + ".d-editor-input", + ` +[chat quote="martin;2321;2022-01-25T05:40:39Z" channel="Cool Cats Club" channelId="1234" multiQuote="true"] +http://www.example.com/has-title.html +[/chat]` + ); + + const rendered = generateTranscriptHTML( + '', + { + messageId: "2321", + username: "martin", + datetime: "2022-01-25T05:40:39Z", + channel: "Cool Cats Club", + channelId: "1234", + multiQuote: true, + linkTabIndex: true, + showDateTimeText: true, + timezone: "Australia/Brisbane", + } + ); + + assert.strictEqual( + query(".d-editor-preview").innerHTML.trim(), + rendered.trim(), + "it renders correctly with the onebox inside the [chat] bbcode" + ); + + const textarea = query("#reply-control .d-editor-input"); + await fillIn(".d-editor-input", textarea.value + "\nA"); + assert.ok( + query(".d-editor-preview").innerHTML.trim().includes("\nA
"), + "it does not error with a opts.discourse.hoisted error in the markdown pipeline when typing more text" + ); + }); + } +); diff --git a/plugins/chat/test/javascripts/acceptance/chat-user-menu-notifications-test.js b/plugins/chat/test/javascripts/acceptance/chat-user-menu-notifications-test.js new file mode 100644 index 0000000000..866def7056 --- /dev/null +++ b/plugins/chat/test/javascripts/acceptance/chat-user-menu-notifications-test.js @@ -0,0 +1,252 @@ +import I18n from "I18n"; +import { test } from "qunit"; + +import { click, visit } from "@ember/test-helpers"; + +import { + acceptance, + exists, + query, + queryAll, + updateCurrentUser, +} from "discourse/tests/helpers/qunit-helpers"; + +import { + baseChatPretenders, + chatChannelPretender, +} from "../helpers/chat-pretenders"; + +acceptance( + "Discourse Chat - experiment user menu notifications - user cannot chat", + function (needs) { + needs.user({ has_chat_enabled: false }); + needs.settings({ chat_enabled: false }); + + test("chat notifications tab is not displayed in user menu", async function (assert) { + await visit("/"); + await click(".header-dropdown-toggle.current-user"); + + assert.notOk( + exists("#user-menu-button-chat-notifications"), + "button for chat notifications tab is not displayed" + ); + }); + } +); + +acceptance( + "Discourse Chat - experimental user menu notifications ", + function (needs) { + needs.user({ redesigned_user_menu_enabled: true, has_chat_enabled: true }); + needs.settings({ chat_enabled: true }); + + needs.pretender((server, helper) => { + baseChatPretenders(server, helper); + chatChannelPretender(server, helper); + }); + + test("chat notifications tab", async function (assert) { + updateCurrentUser({ + grouped_unread_notifications: { + 29: 3, // chat_mention notification type + 31: 1, // chat_invitation notification type + }, + }); + + await visit("/"); + await click(".header-dropdown-toggle.current-user"); + + assert.ok( + exists("#user-menu-button-chat-notifications"), + "button for chat notifications tab is displayed" + ); + + assert.ok( + exists("#user-menu-button-chat-notifications .d-icon-comment"), + "displays the comment icon for chat notification tab button" + ); + + assert.strictEqual( + query("#user-menu-button-chat-notifications .badge-notification") + .textContent, + "4", + "displays the right badge count for chat notifications tab button" + ); + }); + + test("chat mention notification link", async function (assert) { + await visit("/"); + await click(".header-dropdown-toggle.current-user"); + + const chatMentionNotificationLink = queryAll(".chat-mention a")[0]; + + assert.strictEqual( + chatMentionNotificationLink.textContent + .trim() + .replace(/\n/g, "") + .replace(/\s+/, " "), + 'hawk mentioned you in "Site"', + "displays the right text for notification" + ); + + assert.ok( + exists(chatMentionNotificationLink.querySelector(".d-icon-comment")), + "displays the right icon for the notification" + ); + + assert.strictEqual( + chatMentionNotificationLink.title, + I18n.t("notifications.titles.chat_mention"), + "has the right title attribute for notification link" + ); + + assert.ok( + chatMentionNotificationLink.href.endsWith( + "/chat/channel/9/site?messageId=174" + ), + "has the right href attribute for notification link" + ); + }); + + test("personal chat mention notification link", async function (assert) { + await visit("/"); + await click(".header-dropdown-toggle.current-user"); + + const personalChatMentionNotificationLink = + queryAll(".chat-mention a")[3]; + + assert.strictEqual( + personalChatMentionNotificationLink.textContent + .trim() + .replace(/\n/g, "") + .replace(/\s+/, " "), + "hawk mentioned you in personal chat", + "displays the right text for notification" + ); + + assert.ok( + exists( + personalChatMentionNotificationLink.querySelector(".d-icon-comment") + ), + "displays the right icon for the notification" + ); + + assert.strictEqual( + personalChatMentionNotificationLink.title, + I18n.t("notifications.titles.chat_mention"), + "has the right title attribute for notification link" + ); + + assert.ok( + personalChatMentionNotificationLink.href.endsWith( + "/chat/channel/9/site?messageId=174" + ), + "has the right href attribute for notification link" + ); + }); + + test("chat group mention notification link", async function (assert) { + await visit("/"); + await click(".header-dropdown-toggle.current-user"); + + const chatGroupMentionNotificationLink = queryAll(".chat-mention a")[1]; + + assert.strictEqual( + chatGroupMentionNotificationLink.textContent + .trim() + .replace(/\n/g, "") + .replace(/\s+/, " "), + 'hawk mentioned @engineers in "Site"', + "displays the right text for notification" + ); + + assert.ok( + exists( + chatGroupMentionNotificationLink.querySelector(".d-icon-comment") + ), + "displays the right icon for the notification" + ); + + assert.strictEqual( + chatGroupMentionNotificationLink.title, + I18n.t("notifications.titles.chat_mention"), + "has the right title attribute for notification link" + ); + + assert.ok( + chatGroupMentionNotificationLink.href.endsWith( + "/chat/channel/9/site?messageId=174" + ), + "has the right href attribute for notification link" + ); + }); + + test("chat all mention notification link", async function (assert) { + await visit("/"); + await click(".header-dropdown-toggle.current-user"); + + const chatAllMentionNotificationLink = queryAll(".chat-mention a")[2]; + + assert.strictEqual( + chatAllMentionNotificationLink.textContent + .trim() + .replace(/\n/g, "") + .replace(/\s+/, " "), + 'hawk mentioned @all in "Site"', + "displays the right text for notification" + ); + + assert.ok( + exists(chatAllMentionNotificationLink.querySelector(".d-icon-comment")), + "displays the right icon for the notification" + ); + + assert.strictEqual( + chatAllMentionNotificationLink.title, + I18n.t("notifications.titles.chat_mention"), + "has the right title attribute for notification link" + ); + + assert.ok( + chatAllMentionNotificationLink.href.endsWith( + "/chat/channel/9/site?messageId=174" + ), + "has the right href attribute for notification link" + ); + }); + + test("chat invite notification link", async function (assert) { + await visit("/"); + await click(".header-dropdown-toggle.current-user"); + + const chatInviteNotificationLink = queryAll(".chat-invitation a")[0]; + + assert.strictEqual( + chatInviteNotificationLink.textContent + .trim() + .replace(/\n/g, "") + .replace(/\s+/, " "), + "hawk invited you to join a chat channel", + "displays the right text for notification" + ); + + assert.ok( + exists(chatInviteNotificationLink.querySelector(".d-icon-link")), + "displays the right icon for the notification" + ); + + assert.strictEqual( + chatInviteNotificationLink.title, + I18n.t("notifications.titles.chat_invitation"), + "has the right title attribute for notification link" + ); + + assert.ok( + chatInviteNotificationLink.href.endsWith( + "/chat/channel/9/site?messageId=174" + ), + "has the right href attribute for notification link" + ); + }); + } +); diff --git a/plugins/chat/test/javascripts/acceptance/composer-hashtag-autocomplete-test.js b/plugins/chat/test/javascripts/acceptance/composer-hashtag-autocomplete-test.js new file mode 100644 index 0000000000..107f62334d --- /dev/null +++ b/plugins/chat/test/javascripts/acceptance/composer-hashtag-autocomplete-test.js @@ -0,0 +1,68 @@ +import { setCaretPosition } from "discourse/lib/utilities"; +import { + acceptance, + exists, + query, + queryAll, +} from "discourse/tests/helpers/qunit-helpers"; +import { test } from "qunit"; +import { chatChannelPretender } from "../helpers/chat-pretenders"; +import { fillIn, settled, triggerKeyEvent, visit } from "@ember/test-helpers"; + +acceptance( + "Discourse Chat - Composer hashtag autocompletion", + function (needs) { + needs.user({ + admin: false, + moderator: false, + username: "eviltrout", + id: 100, + can_chat: true, + has_chat_enabled: true, + }); + needs.pretender((server, helper) => { + chatChannelPretender(server, helper); + server.get("/chat/:id/messages.json", () => + helper.response({ chat_messages: [], meta: {} }) + ); + server.post("/chat/drafts", () => helper.response(500, {})); + server.get("/hashtags/search.json", () => { + return helper.response({ + results: [ + { type: "category", text: "Design", slug: "design", ref: "design" }, + { type: "tag", text: "dev", slug: "dev", ref: "dev" }, + { type: "tag", text: "design", slug: "design", ref: "design::tag" }, + ], + }); + }); + }); + needs.settings({ + chat_enabled: true, + enable_experimental_hashtag_autocomplete: true, + }); + + test("using # in the chat composer shows category and tag autocomplete options", async function (assert) { + await visit("/chat/channel/11/-"); + const composerInput = query(".chat-composer-input"); + await fillIn(".chat-composer-input", "abc #"); + await triggerKeyEvent(".chat-composer-input", "keydown", "#"); + await fillIn(".chat-composer-input", "abc #"); + await setCaretPosition(composerInput, 5); + await triggerKeyEvent(".chat-composer-input", "keyup", "#"); + await triggerKeyEvent(".chat-composer-input", "keydown", "D"); + await fillIn(".chat-composer-input", "abc #d"); + await setCaretPosition(composerInput, 6); + await triggerKeyEvent(".chat-composer-input", "keyup", "D"); + await settled(); + assert.ok( + exists(".hashtag-autocomplete"), + "hashtag autocomplete menu appears" + ); + assert.strictEqual( + queryAll(".hashtag-autocomplete__option").length, + 3, + "all options should be shown" + ); + }); + } +); diff --git a/plugins/chat/test/javascripts/acceptance/core-sidebar-test.js b/plugins/chat/test/javascripts/acceptance/core-sidebar-test.js new file mode 100644 index 0000000000..4cf7fa9726 --- /dev/null +++ b/plugins/chat/test/javascripts/acceptance/core-sidebar-test.js @@ -0,0 +1,680 @@ +import { + acceptance, + exists, + query, + queryAll, +} from "discourse/tests/helpers/qunit-helpers"; +import { test } from "qunit"; +import { click, currentURL, settled, visit } from "@ember/test-helpers"; +import { directMessageChannels } from "discourse/plugins/chat/chat-fixtures"; +import { cloneJSON } from "discourse-common/lib/object"; +import I18n from "I18n"; +import { withPluginApi } from "discourse/lib/plugin-api"; +import { emojiUnescape } from "discourse/lib/text"; +import User from "discourse/models/user"; + +acceptance("Discourse Chat - Core Sidebar", function (needs) { + needs.user({ has_chat_enabled: true }); + + needs.settings({ + chat_enabled: true, + enable_experimental_sidebar_hamburger: true, + enable_sidebar: true, + }); + + needs.pretender((server, helper) => { + let directChannels = cloneJSON(directMessageChannels).mapBy("chat_channel"); + directChannels[0].chatable.users = [directChannels[0].chatable.users[0]]; + directChannels[0].current_user_membership.unread_count = 1; + directChannels.push({ + chatable: { + users: [ + { + id: 1, + username: "markvanlan", + avatar_template: + "/letter_avatar_proxy/v4/letter/t/f9ae1b/{size}.png", + }, + { + id: 2, + username: "sam", + avatar_template: + "/letter_avatar_proxy/v4/letter/t/f9ae1b/{size}.png", + }, + ], + }, + chatable_id: 59, + chatable_type: "DirectMessage", + chatable_url: null, + id: 76, + title: "@sam", + last_message_sent_at: "2021-06-01T11:15:00.000Z", + current_user_membership: { + unread_count: 0, + muted: true, + following: true, + }, + }); + directChannels.push({ + chatable: { + users: [ + { + id: 1, + username: "", + avatar_template: + "/letter_avatar_proxy/v4/letter/t/f9ae1b/{size}.png", + }, + { + id: 2, + username: "", + avatar_template: + "/letter_avatar_proxy/v4/letter/t/f9ae1b/{size}.png", + }, + ], + }, + chatable_type: "DirectMessage", + chatable_url: null, + id: 77, + title: "@", + last_message_sent_at: "2021-06-01T11:15:00.000Z", + current_user_membership: { + unread_count: 0, + muted: false, + following: true, + }, + }); + + server.get("/chat/chat_channels.json", () => { + return helper.response({ + public_channels: [ + { + id: 1, + title: "dev :bug:", + chatable_type: "Category", + chatable: { slug: "dev", read_restricted: true }, + last_message_sent_at: "2021-11-08T21:26:05.710Z", + current_user_membership: { + unread_count: 0, + unread_mentions: 0, + }, + }, + { + id: 2, + title: "general", + chatable_type: "Category", + chatable: { slug: "general" }, + last_message_sent_at: "2021-11-08T21:26:05.710Z", + current_user_membership: { + unread_count: 1, + unread_mentions: 0, + }, + }, + { + id: 3, + title: "random", + chatable_type: "Category", + chatable: { slug: "random" }, + last_message_sent_at: "2021-11-08T21:26:05.710Z", + current_user_membership: { + muted: true, + unread_count: 1, + unread_mentions: 1, + }, + }, + { + id: 4, + title: "", + chatable_type: "Category", + chatable: { slug: "random" }, + last_message_sent_at: "2021-11-08T21:26:05.710Z", + current_user_membership: { + unread_count: 1, + unread_mentions: 1, + }, + }, + ], + direct_message_channels: directChannels, + }); + }); + + server.get("/chat/1/messages.json", () => + helper.response({ + meta: { can_chat: true, user_silenced: false }, + chat_messages: [], + }) + ); + + server.get("/u/search/users", () => { + return helper.response({ + users: [ + { + username: "hawk", + id: 2, + name: "hawk", + avatar_template: + "/letter_avatar_proxy/v4/letter/t/41988e/{size}.png", + }, + ], + }); + }); + + server.get("/chat/75/messages.json", () => + helper.response({ + meta: { can_chat: true, user_silenced: false }, + chat_messages: [], + }) + ); + + server.get("/chat/direct_messages.json", () => { + return helper.response({ + chat_channel: { + id: 75, + title: "hawk", + chatable_type: "DirectMessage", + last_message_sent_at: "2021-07-20T08:14:16.950Z", + chatable: { + users: [{ username: "hawk" }], + }, + }, + }); + }); + }); + + needs.hooks.beforeEach(function () { + withPluginApi("1.3.0", (api) => { + api.addUsernameSelectorDecorator((username) => { + if (username === "hawk") { + return `${emojiUnescape( + ":desert_island:" + )}`; + } + }); + }); + }); + + test("Public channels section", async function (assert) { + await visit("/"); + + assert.strictEqual( + query( + ".sidebar-section-chat-channels .sidebar-section-header-text" + ).textContent.trim(), + I18n.t("chat.chat_channels"), + "displays correct channels section title" + ); + + assert.ok( + exists( + ".sidebar-section-chat-channels .sidebar-section-link-dev-bug .sidebar-section-link-prefix svg.prefix-icon.d-icon-hashtag" + ), + "dev channel section link displays hash icon prefix" + ); + + assert.ok( + exists( + ".sidebar-section-chat-channels .sidebar-section-link-dev-bug .sidebar-section-link-prefix svg.prefix-badge.d-icon-lock" + ), + "dev channel section link displays lock badge for restricted channel" + ); + + assert.ok( + exists( + ".sidebar-section-chat-channels .sidebar-section-link-dev-bug .emoji" + ), + "unescapes emoji in channel title in the link" + ); + + assert.strictEqual( + query( + ".sidebar-section-chat-channels .sidebar-section-link-dev-bug" + ).textContent.trim(), + "dev", + "dev channel section link displays channel title in the link" + ); + + assert.ok( + query( + ".sidebar-section-chat-channels .sidebar-section-link-dev-bug" + ).href.endsWith("/chat/channel/1/dev-bug"), + "dev channel section link has the right href attribute" + ); + + assert.notOk( + exists( + ".sidebar-section-chat-channels .sidebar-section-link-dev-bug .sidebar-section-link-suffix" + ), + "does not display new messages indicator" + ); + + assert.ok( + exists( + ".sidebar-section-chat-channels .sidebar-section-link-general .sidebar-section-link-prefix svg.prefix-icon.d-icon-hashtag" + ), + "general channel section link displays hash icon prefix" + ); + + assert.notOk( + exists( + ".sidebar-section-chat-channels .sidebar-section-link-general .sidebar-section-link-prefix svg.prefix-badge" + ), + "general channel section link does not display lock badge for public channel" + ); + + assert.strictEqual( + query( + ".sidebar-section-chat-channels .sidebar-section-link-general" + ).textContent.trim(), + "general", + "general channel section link displays channel title in the link" + ); + + assert.ok( + exists( + ".sidebar-section-chat-channels .sidebar-section-link-general .sidebar-section-link-suffix.unread" + ), + "general section link has new messages indicator" + ); + + assert.ok( + exists( + ".sidebar-section-chat-channels .sidebar-section-link-random .sidebar-section-link-prefix svg.prefix-icon.d-icon-hashtag" + ), + "random channel section link displays hash icon prefix" + ); + + assert.strictEqual( + query( + ".sidebar-section-chat-channels .sidebar-section-link-random" + ).textContent.trim(), + "random", + "random channel section link displays channel title in the link" + ); + + assert.ok( + exists( + ".sidebar-section-chat-channels .sidebar-section-link-random .sidebar-section-link-suffix.urgent" + ), + "random section link has new messages mention indicator" + ); + }); + + test("sidebar section link when direct message channel is muted by user", async function (assert) { + await visit("/"); + + assert.ok( + exists( + ".sidebar-section-chat-dms .sidebar-section-link-sam.sidebar-section-link--muted" + ), + "muted direct chat channel section link has right classname configured" + ); + }); + + test("sidebar section link when public channel is muted by user", async function (assert) { + await visit("/"); + + assert.ok( + exists( + ".sidebar-section-chat-channels .sidebar-section-link-random.sidebar-section-link--muted" + ), + "muted random chat channel section link has right classname configured" + ); + }); + + test("Direct messages section", async function (assert) { + const chatService = this.container.lookup("service:chat"); + chatService.directMessagesLimit = 2; + await visit("/"); + + assert.strictEqual( + query( + ".sidebar-section-chat-dms .sidebar-section-header-text" + ).textContent.trim(), + I18n.t("chat.direct_messages.title"), + "displays correct direct messages section title" + ); + + let directLinks = queryAll( + ".sidebar-section-chat-dms a.sidebar-section-link" + ); + + assert.strictEqual( + directLinks[0] + .querySelector(".sidebar-section-link-prefix img") + .classList.contains("prefix-image"), + true, + "displays avatar in prefix when two participants" + ); + + assert.strictEqual( + directLinks[0].textContent.trim(), + "hawk", + "displays user name in a link" + ); + + assert.ok( + directLinks[0].querySelector( + ".sidebar-section-link-content-text .on-holiday img" + ), + "displays flair when user is on holiday" + ); + + assert.strictEqual( + directLinks[0] + .querySelector(".sidebar-section-link-suffix") + .classList.contains("urgent"), + true, + "displays new messages indicator" + ); + + assert.strictEqual( + directLinks[1] + .querySelector("span.sidebar-section-link-prefix") + .classList.contains("text"), + true, + "displays text in prefix when more than two participants" + ); + + assert.strictEqual( + directLinks[1] + .querySelector(".sidebar-section-link-content-text") + .textContent.trim(), + "eviltrout, markvanlan", + "displays all participants name in a link" + ); + + assert.ok( + !directLinks[1].querySelector(".sidebar-section-link-suffix"), + "does not display new messages indicator" + ); + User.current().chat_channel_tracking_state[76].set("unread_count", 99); + chatService.reSortDirectMessageChannels(); + chatService.appEvents.trigger("chat:user-tracking-state-changed"); + await settled(); + + directLinks = queryAll(".sidebar-section-chat-dms a.sidebar-section-link"); + assert.strictEqual( + directLinks[0] + .querySelector(".sidebar-section-link-content-text") + .textContent.trim(), + "eviltrout, markvanlan", + "reorders private messages" + ); + + assert.equal( + directLinks.length, + 2, + "limits number of displayed direct messages" + ); + }); + + test("Plugin sidebar is hidden", async function (assert) { + await visit("/chat/channel/1/dev"); + assert.notOk(exists(".full-page-chat .channels-list")); + }); + + test("Open a new direct conversation", async function (assert) { + await visit("/"); + await click(".sidebar-section-chat-dms .sidebar-section-header-button"); + + assert.ok(exists(".direct-message-creator")); + assert.ok(exists(".topic-chat-container.expanded.visible")); + assert.strictEqual(currentURL(), "/"); + }); + + test("Escapes public channel titles", async function (assert) { + await visit("/"); + + const evilChannel = query( + ".sidebar-section-chat-channels .sidebar-section-link-wrapper .sidebar-section-link" + ); + + assert.strictEqual(evilChannel.title, "<script>evil</script>"); + + assert.ok( + evilChannel.className.includes( + "sidebar-section-link-ltscriptgtevilltscriptgt" + ) + ); + + assert.strictEqual( + evilChannel + .querySelector(".sidebar-section-link-content-text") + .innerHTML.trim(), + "<script>evil</script>" + ); + }); + + test("Escapes dm channel titles", async function (assert) { + await visit("/"); + + const evilChannel = queryAll( + ".sidebar-section-chat-dms .sidebar-section-link-wrapper .sidebar-section-link" + )[3]; + + assert.strictEqual(evilChannel.title, "@<script>sam</script>"); + + assert.ok( + evilChannel.className.includes( + "sidebar-section-link-ltscriptgtsamltscriptgt" + ) + ); + + assert.strictEqual( + evilChannel + .querySelector(".sidebar-section-link-content-text") + .innerHTML.trim(), + "<script>sam</script>" + ); + }); +}); + +acceptance("Discourse Chat - Plugin Sidebar", function (needs) { + needs.user({ has_chat_enabled: true }); + + needs.settings({ + chat_enabled: true, + enable_sidebar: false, + }); + + needs.pretender((server, helper) => { + server.get("/chat/chat_channels.json", () => { + return helper.response({ + public_channels: [ + { + id: 1, + title: "dev :bug:", + chatable_type: "Category", + chatable: { slug: "dev", read_restricted: true }, + last_message_sent_at: "2021-11-08T21:26:05.710Z", + current_user_membership: { + unread_count: 1, + unread_mentions: 1, + }, + }, + { + id: 2, + title: "general", + chatable_type: "Category", + chatable: { slug: "general" }, + last_message_sent_at: "2021-11-08T21:26:05.710Z", + current_user_membership: { + unread_count: 1, + unread_mentions: 1, + }, + }, + { + id: 3, + title: "random", + chatable_type: "Category", + chatable: { slug: "random" }, + last_message_sent_at: "2021-11-08T21:26:05.710Z", + current_user_membership: { + unread_count: 1, + unread_mentions: 1, + }, + }, + ], + direct_message_channels: [], + }); + }); + + server.get("/chat/1/messages.json", () => + helper.response({ + meta: { can_chat: true, user_silenced: false }, + chat_messages: [], + }) + ); + }); + + test("Plugin sidebar is visible", async function (assert) { + await visit("/chat/channel/1/dev"); + assert.ok(exists(".full-page-chat .channels-list")); + }); +}); + +acceptance( + "Discourse Chat - Core Sidebar - no joinable public channels, staff", + function (needs) { + needs.user({ has_chat_enabled: true, has_joinable_public_channels: false }); + + needs.settings({ + chat_enabled: true, + enable_experimental_sidebar_hamburger: true, + enable_sidebar: true, + }); + + needs.pretender((server, helper) => { + server.get("/chat/chat_channels.json", () => { + return helper.response({ + public_channels: [], + direct_message_channels: [], + }); + }); + }); + + test("Chat channels section visibility", async function (assert) { + await visit("/"); + + assert.ok( + exists(".sidebar-section-chat-channels"), + "it shows the section for staff" + ); + }); + } +); + +acceptance( + "Discourse Chat - Core Sidebar - no joinable public channels, regular user", + function (needs) { + needs.user({ + has_chat_enabled: true, + has_joinable_public_channels: false, + moderator: false, + admin: false, + }); + + needs.settings({ + chat_enabled: true, + enable_experimental_sidebar_hamburger: true, + enable_sidebar: true, + }); + + needs.pretender((server, helper) => { + server.get("/chat/chat_channels.json", () => { + return helper.response({ + public_channels: [], + direct_message_channels: [], + }); + }); + }); + + test("Chat channels section visibility", async function (assert) { + await visit("/"); + + assert.notOk( + exists(".sidebar-section-chat-channels"), + "it doesn’t show the section for regular user" + ); + }); + } +); + +acceptance( + "Discourse Chat - Core Sidebar - regular user with no direct message channels who cannot send direct messages", + function (needs) { + needs.user({ + has_chat_enabled: true, + moderator: false, + admin: false, + }); + + needs.settings({ + chat_enabled: true, + enable_experimental_sidebar_hamburger: true, + enable_sidebar: true, + direct_message_enabled_groups: "13", // trust_level_3 auto group ID; + }); + + needs.pretender((server, helper) => { + server.get("/chat/chat_channels.json", () => { + return helper.response({ + public_channels: [], + direct_message_channels: [], + }); + }); + }); + + test("Direct message channels section visibility", async function (assert) { + await visit("/"); + + assert.notOk( + exists(".sidebar-section-chat-dms"), + "it doesn’t show the section for regular user" + ); + }); + } +); + +acceptance( + "Discourse Chat - Core Sidebar - regular user with existing direct message channels who cannot send direct messages", + function (needs) { + needs.user({ + has_chat_enabled: true, + moderator: false, + admin: false, + }); + + needs.settings({ + chat_enabled: true, + enable_experimental_sidebar_hamburger: true, + enable_sidebar: true, + direct_message_enabled_groups: "13", // trust_level_3 auto group ID; + }); + + needs.pretender((server, helper) => { + let directChannels = cloneJSON(directMessageChannels).mapBy( + "chat_channel" + ); + server.get("/chat/chat_channels.json", () => { + return helper.response({ + public_channels: [], + direct_message_channels: directChannels, + }); + }); + }); + + test("Direct message channels section visibility", async function (assert) { + await visit("/"); + + assert.ok( + exists(".sidebar-section-chat-dms"), + "it does show the section for a regular user" + ); + + assert.notOk( + exists(".sidebar-section-chat-dms .sidebar-section-header-button"), + "user cannot see the create DM channel button" + ); + }); + } +); diff --git a/plugins/chat/test/javascripts/acceptance/create-channel-test.js b/plugins/chat/test/javascripts/acceptance/create-channel-test.js new file mode 100644 index 0000000000..a478f2225c --- /dev/null +++ b/plugins/chat/test/javascripts/acceptance/create-channel-test.js @@ -0,0 +1,179 @@ +import selectKit from "discourse/tests/helpers/select-kit-helper"; +import { click, visit } from "@ember/test-helpers"; +import { acceptance, query } from "discourse/tests/helpers/qunit-helpers"; +import { test } from "qunit"; + +acceptance("Discourse Chat - Create channel modal", function (needs) { + const maliciousText = '"'; + + needs.user({ + username: "tomtom", + id: 1, + can_chat: true, + has_chat_enabled: true, + }); + + needs.settings({ + chat_enabled: true, + }); + + const catsCategory = { + id: 1, + name: "Cats", + slug: "cats", + permission: 1, + }; + + needs.site({ + categories: [ + catsCategory, + { + id: 2, + name: maliciousText, + slug: maliciousText, + permission: 1, + }, + { + id: 3, + name: "Kittens", + slug: "kittens", + permission: 1, + parentCategory: catsCategory, + }, + ], + }); + + needs.pretender((server, helper) => { + server.get("/chat/:chatChannelId/messages.json", () => + helper.response({ + meta: { can_chat: true, user_silenced: false }, + chat_messages: [], + }) + ); + + server.get("/chat/chat_channels.json", () => + helper.response({ + public_channels: [], + direct_message_channels: [], + }) + ); + + server.get("/chat/chat_channels/:chatChannelId", () => + helper.response({ id: 1, title: "something" }) + ); + + server.get("/chat/api/chat_channels.json", () => helper.response([])); + + server.get( + "/chat/api/category-chatables/:categoryId/permissions.json", + (request) => { + if (request.params.categoryId === "2") { + return helper.response({ + allowed_groups: ["@"], + members_count: 2, + private: true, + }); + } else { + return helper.response({ + allowed_groups: ["@awesomeGroup"], + members_count: 2, + private: true, + }); + } + } + ); + }); + + test("links to categories and selected category's security settings", async function (assert) { + await visit("/chat/browse"); + await click(".new-channel-btn"); + + assert.strictEqual( + query(".create-channel-hint a").innerText, + "category security settings" + ); + assert.ok(query(".create-channel-hint a").href.includes("/categories")); + + let categories = selectKit(".create-channel-modal .category-chooser"); + await categories.expand(); + await categories.selectRowByName("Cats"); + + assert.strictEqual( + query(".create-channel-hint a").innerText, + "security settings" + ); + assert.ok( + query(".create-channel-hint a").href.includes("/c/cats/edit/security") + ); + }); + + test("links to selected category's security settings works with nested subcategories", async function (assert) { + await visit("/chat/browse"); + await click(".new-channel-btn"); + + assert.strictEqual( + query(".create-channel-hint a").innerText, + "category security settings" + ); + assert.ok(query(".create-channel-hint a").href.includes("/categories")); + + let categories = selectKit(".create-channel-modal .category-chooser"); + await categories.expand(); + await categories.selectRowByName("Kittens"); + + assert.strictEqual( + query(".create-channel-hint a").innerText, + "security settings" + ); + assert.ok( + query(".create-channel-hint a").href.includes( + "/c/cats/kittens/edit/security" + ) + ); + }); + + test("includes group names in the hint", async (assert) => { + await visit("/chat/browse"); + await click(".new-channel-btn"); + + assert.strictEqual( + query(".create-channel-hint a").innerText, + "category security settings" + ); + assert.ok(query(".create-channel-hint a").href.includes("/categories")); + + let categories = selectKit(".create-channel-modal .category-chooser"); + await categories.expand(); + await categories.selectRowByName("Kittens"); + + assert.strictEqual( + query(".create-channel-hint").innerHTML.trim(), + 'Users in @awesomeGroup will have access to this channel per the security settings' + ); + }); + + test("escapes group name/category slug in the hint", async (assert) => { + await visit("/chat/browse"); + await click(".new-channel-btn"); + + assert.strictEqual( + query(".create-channel-hint a").innerText, + "category security settings" + ); + assert.ok(query(".create-channel-hint a").href.includes("/categories")); + + const categories = selectKit(".create-channel-modal .category-chooser"); + await categories.expand(); + await categories.selectRowByValue(2); + + assert.strictEqual( + query(".create-channel-hint").innerHTML.trim(), + 'Users in @<script>evilgroup</script> will have access to this channel per the security settings' + ); + assert.ok( + query(".create-channel-hint a").href.includes( + "c/%22%3Cscript%3E%3C/script%3E/edit/security" + ) + ); + }); +}); diff --git a/plugins/chat/test/javascripts/acceptance/delete-chat-channel-modal-test.js b/plugins/chat/test/javascripts/acceptance/delete-chat-channel-modal-test.js new file mode 100644 index 0000000000..7510ae1450 --- /dev/null +++ b/plugins/chat/test/javascripts/acceptance/delete-chat-channel-modal-test.js @@ -0,0 +1,42 @@ +import { acceptance } from "discourse/tests/helpers/qunit-helpers"; +import { click, currentURL, fillIn, visit } from "@ember/test-helpers"; +import { test } from "qunit"; +import fabricators from "../helpers/fabricators"; + +acceptance("Discourse Chat - delete chat channel modal", function (needs) { + needs.user({ has_chat_enabled: true, can_chat: true }); + + needs.settings({ chat_enabled: true }); + + needs.pretender((server, helper) => { + server.get("/chat/chat_channels.json", () => { + return helper.response({ + public_channels: [fabricators.chatChannel({ id: 2 })], + direct_message_channels: [], + }); + }); + + server.get("/chat/chat_channels/:id", (request) => { + return helper.response( + fabricators.chatChannel({ id: request.params.id }) + ); + }); + + server.get("/chat/:id/messages.json", () => { + return helper.response({ meta: {}, chat_messages: [] }); + }); + + server.delete("/chat/chat_channels/:id.json", () => { + return helper.response({}); + }); + }); + + test("Redirection after deleting a channel", async function (assert) { + await visit("chat/channel/1/my-category-title/info/settings"); + await click(".delete-btn"); + await fillIn("#channel-delete-confirm-name", "My category title"); + await click("#chat-confirm-delete-channel"); + + assert.equal(currentURL(), "/chat/channel/2/my-category-title"); + }); +}); diff --git a/plugins/chat/test/javascripts/acceptance/mobile-chat-test.js b/plugins/chat/test/javascripts/acceptance/mobile-chat-test.js new file mode 100644 index 0000000000..62db7c85bb --- /dev/null +++ b/plugins/chat/test/javascripts/acceptance/mobile-chat-test.js @@ -0,0 +1,75 @@ +import { + acceptance, + exists, + loggedInUser, +} from "discourse/tests/helpers/qunit-helpers"; +import { click, currentURL, visit } from "@ember/test-helpers"; +import { + chatChannels, + generateChatView, +} from "discourse/plugins/chat/chat-fixtures"; +import { test } from "qunit"; + +acceptance("Discourse Chat - Mobile test", function (needs) { + needs.user({ can_chat: true, has_chat_enabled: true }); + + needs.mobileView(); + + needs.pretender((server, helper) => { + server.get("/chat/chat_channels.json", () => helper.response(chatChannels)); + server.get("/chat/:id/messages.json", () => + helper.response(generateChatView(loggedInUser())) + ); + server.get("/u/search/users", () => { + return helper.response([]); + }); + + server.get("/chat/api/chat_channels.json", () => { + const channels = []; + return helper.response(channels); + }); + }); + + needs.settings({ + chat_enabled: true, + }); + + test("Chat index route shows channels list", async function (assert) { + await visit("/latest"); + await click(".header-dropdown-toggle.open-chat"); + assert.equal(currentURL(), "/chat"); + assert.ok(exists(".channels-list")); + await click(".chat-channel-row.chat-channel-7"); + assert.notOk(exists(".open-drawer-btn")); + }); + + test("Chat new personal chat buttons", async function (assert) { + await visit("/chat"); + await click(".open-draft-channel-page-btn.btn-floating"); + + assert.strictEqual( + currentURL(), + "/chat/draft-channel", + "Clicking the floating + button opens the new chat screen" + ); + + await click(".chat-draft-header__btn"); + + assert.strictEqual( + currentURL(), + "/chat", + "Clicking the left arrow button returns to the channels list" + ); + }); + + test("Chat browse screen back button", async function (assert) { + await visit("/chat/browse"); + await click(".chat-full-page-header__back-btn"); + + assert.strictEqual( + currentURL(), + "/chat", + "Clicking the back button returns to the channels list" + ); + }); +}); diff --git a/plugins/chat/test/javascripts/acceptance/user-card-chat-test.js b/plugins/chat/test/javascripts/acceptance/user-card-chat-test.js new file mode 100644 index 0000000000..a4750fea4e --- /dev/null +++ b/plugins/chat/test/javascripts/acceptance/user-card-chat-test.js @@ -0,0 +1,117 @@ +import userFixtures from "discourse/tests/fixtures/user-fixtures"; +import { cloneJSON } from "discourse-common/lib/object"; +import { + acceptance, + exists, + loggedInUser, + query, + visible, +} from "discourse/tests/helpers/qunit-helpers"; +import { click, visit } from "@ember/test-helpers"; +import { + chatChannels, + directMessageChannels, + generateChatView, +} from "discourse/plugins/chat/chat-fixtures"; +import { test } from "qunit"; + +acceptance("Discourse Chat - User card test", function (needs) { + needs.user({ + admin: false, + moderator: false, + username: "eviltrout", + id: 1, + can_chat: true, + has_chat_enabled: true, + }); + needs.pretender((server, helper) => { + server.post("/uploads/lookup-urls", () => { + return helper.response([]); + }); + server.get("/chat/chat_channels.json", () => helper.response(chatChannels)); + server.get("/chat/chat_channels/:channelId.json", () => + helper.response(helper.response(directMessageChannels[0])) + ); + server.get("/chat/:chatChannelId/messages.json", () => + helper.response(generateChatView(loggedInUser())) + ); + server.post("/chat/direct_messages/create.json", () => { + return helper.response({ + chat_channel: { + chat_channels: [], + chatable: { + users: [ + { + username: "hawk", + id: 2, + name: "hawk", + avatar_template: + "/letter_avatar_proxy/v3/letter/t/41988e/{size}.png", + }, + ], + }, + chatable_id: 16, + chatable_type: "DirectMessage", + chatable_url: null, + id: 75, + title: "@hawk", + last_message_sent_at: "2021-11-08T21:26:05.710Z", + current_user_membership: { + last_read_message_id: null, + unread_count: 0, + unread_mentions: 0, + }, + }, + }); + }); + let cardResponse = cloneJSON(userFixtures["/u/charlie/card.json"]); + cardResponse.user.can_chat_user = true; + server.get("/u/hawk/card.json", () => helper.response(cardResponse)); + }); + needs.settings({ + chat_enabled: true, + }); + + needs.hooks.beforeEach(function () { + Object.defineProperty(this, "chatService", { + get: () => this.container.lookup("service:chat"), + }); + Object.defineProperty(this, "appEvents", { + get: () => this.container.lookup("service:appEvents"), + }); + }); + + test("user card has chat button that opens the correct channel", async function (assert) { + this.chatService.set("sidebarActive", false); + await visit("/"); + await click(".header-dropdown-toggle.open-chat"); + await click(".chat-channel-row.chat-channel-9"); + await click("[data-user-card='hawk']"); + + assert.ok(exists(".user-card-chat-btn")); + + await click(".user-card-chat-btn"); + + assert.ok(visible(".topic-chat-float-container"), "chat float is open"); + assert.ok(query(".topic-chat-container").classList.contains("channel-75")); + }); +}); + +acceptance( + "Discourse Chat - Anon user viewing user card test", + function (needs) { + needs.settings({ + chat_enabled: true, + }); + + test("user card has no chat button", async function (assert) { + await visit("/t/internationalization-localization/280"); + await click('a[data-user-card="charlie"]'); + + assert.notOk( + exists(".user-card-chat-btn"), + "anon user should not be able to chat with anyone via the user card" + ); + }); + } +); diff --git a/plugins/chat/test/javascripts/chat-fixtures.js b/plugins/chat/test/javascripts/chat-fixtures.js new file mode 100644 index 0000000000..c8ab615160 --- /dev/null +++ b/plugins/chat/test/javascripts/chat-fixtures.js @@ -0,0 +1,334 @@ +import { deepMerge } from "discourse-common/lib/object"; + +export const messageContents = ["Hello world", "What up", "heyo!"]; + +export const directMessageChannels = [ + { + chat_channel: { + chatable: { + users: [ + { + id: 1, + username: "markvanlan", + avatar_template: + "/letter_avatar_proxy/v4/letter/t/f9ae1b/{size}.png", + }, + { + id: 2, + username: "hawk", + avatar_template: + "/letter_avatar_proxy/v4/letter/t/f9ae1b/{size}.png", + }, + ], + }, + chatable_id: 58, + chatable_type: "DirectMessage", + chatable_url: null, + id: 75, + title: "@hawk", + current_user_membership: { + unread_count: 0, + muted: false, + following: true, + }, + last_message_sent_at: "2021-07-20T08:14:16.950Z", + }, + }, + { + chat_channel: { + chatable: { + users: [ + { + id: 1, + username: "markvanlan", + avatar_template: + "/letter_avatar_proxy/v4/letter/t/f9ae1b/{size}.png", + }, + { + id: 3, + username: "eviltrout", + avatar_template: + "/letter_avatar_proxy/v4/letter/t/f9ae1b/{size}.png", + }, + ], + }, + chatable_id: 59, + chatable_type: "DirectMessage", + chatable_url: null, + id: 76, + title: "@eviltrout, @markvanlan", + current_user_membership: { + unread_count: 0, + muted: false, + following: true, + }, + last_message_sent_at: "2021-07-05T12:04:00.850Z", + }, + }, +]; + +const chatables = { + 1: { + id: 1, + name: "Bug", + color: "0088CC", + text_color: "FFFFFF", + slug: "bug", + }, + 8: { + id: 8, + name: "Public category", + slug: "public-category", + posts_count: 1, + }, + 12: { + id: 12, + name: "Another category", + slug: "another-category", + posts_count: 100, + }, +}; + +export const chatChannels = { + public_channels: [ + { + id: 9, + chatable_id: 1, + chatable_type: "Category", + chatable_url: "/c/bug/1", + title: "Site", + status: "open", + chatable: chatables[1], + last_message_sent_at: "2021-07-24T08:14:16.950Z", + current_user_membership: { + unread_count: 0, + muted: false, + following: true, + }, + }, + { + id: 7, + chatable_id: 1, + chatable_type: "Category", + chatable_url: "/c/bug/1", + title: "Bug", + status: "open", + chatable: chatables[1], + last_message_sent_at: "2021-07-15T08:14:16.950Z", + current_user_membership: { + unread_count: 0, + muted: false, + following: true, + }, + }, + { + id: 4, + chatable_id: 8, + chatable_type: "Category", + chatable_url: "/c/public-category/8", + title: "Public category", + status: "open", + chatable: chatables[8], + last_message_sent_at: "2021-07-14T08:14:16.950Z", + current_user_membership: { + unread_count: 0, + muted: false, + following: true, + }, + }, + { + id: 5, + chatable_id: 8, + chatable_type: "Category", + chatable_url: "/c/public-category/8", + title: "Public category (read-only)", + status: "read_only", + chatable: chatables[8], + last_message_sent_at: "2021-07-10T08:14:16.950Z", + current_user_membership: { + unread_count: 0, + muted: false, + following: true, + }, + }, + { + id: 6, + chatable_id: 8, + chatable_type: "Category", + chatable_url: "/c/public-category/8", + title: "Public category (closed)", + status: "closed", + chatable: chatables[8], + last_message_sent_at: "2021-07-21T08:14:16.950Z", + current_user_membership: { + unread_count: 0, + muted: false, + following: true, + }, + }, + { + id: 10, + chatable_id: 8, + chatable_type: "Category", + chatable_url: "/c/public-category/8", + title: "Public category (archived)", + status: "archived", + chatable: chatables[8], + last_message_sent_at: "2021-07-25T08:14:16.950Z", + current_user_membership: { + unread_count: 0, + muted: false, + following: true, + }, + }, + { + id: 11, + chatable_id: 12, + chatable_type: "Category", + chatable_url: "/c/another-category/12", + title: "Another Category", + status: "open", + chatable: chatables[12], + last_message_sent_at: "2021-07-02T08:14:16.950Z", + current_user_membership: { + unread_count: 0, + muted: false, + following: true, + }, + }, + ], + direct_message_channels: directMessageChannels.mapBy("chat_channel"), +}; + +const message0 = { + id: 174, + message: messageContents[0], + cooked: messageContents[0], + excerpt: messageContents[0], + created_at: "2021-07-20T08:14:16.950Z", + flag_count: 0, + user: { + id: 1, + username: "markvanlan", + name: null, + avatar_template: "/letter_avatar_proxy/v4/letter/m/48db29/{size}.png", + }, + available_flags: ["spam"], +}; + +const message1 = { + id: 175, + message: messageContents[1], + cooked: messageContents[1], + excerpt: messageContents[1], + created_at: "2021-07-20T08:14:22.043Z", + flag_count: 0, + user: { + id: 2, + username: "hawk", + name: null, + avatar_template: "/letter_avatar_proxy/v4/letter/m/48db29/{size}.png", + }, + in_reply_to: message0, + uploads: [ + { + extension: "pdf", + filesize: 861550, + height: null, + human_filesize: "841 KB", + id: 38, + original_filename: "Chat message PDF!", + retain_hours: null, + short_path: "/uploads/short-url/vYozObYao54I6G3x8wvOf73epfX.pdf", + short_url: "upload://vYozObYao54I6G3x8wvOf73epfX.pdf", + thumbnail_height: null, + thumbnail_width: null, + url: "/images/avatar.png", + width: null, + }, + ], + available_flags: ["spam"], +}; + +const message2 = { + id: 176, + message: messageContents[2], + cooked: messageContents[2], + excerpt: messageContents[2], + created_at: "2021-07-20T08:14:25.043Z", + flag_count: 0, + user: { + id: 2, + username: "hawk", + name: null, + avatar_template: "/letter_avatar_proxy/v4/letter/m/48db29/{size}.png", + }, + in_reply_to: message0, + uploads: [ + { + extension: "png", + filesize: 50419, + height: 393, + human_filesize: "49.2 KB", + id: 37, + original_filename: "image.png", + retain_hours: null, + short_path: "/uploads/short-url/2LbadI7uOM7JsXyVoc12dHUjJYo.png", + short_url: "upload://2LbadI7uOM7JsXyVoc12dHUjJYo.png", + thumbnail_height: 224, + thumbnail_width: 689, + url: "/images/avatar.png", + width: 1209, + }, + ], + reactions: { + heart: { + count: 1, + reacted: false, + users: [{ id: 99, username: "im-penar" }], + }, + kiwi_fruit: { + count: 2, + reacted: true, + users: [{ id: 99, username: "im-penar" }], + }, + tada: { + count: 1, + reacted: true, + users: [], + }, + }, + available_flags: ["spam"], +}; + +const message3 = { + id: 177, + message: "gg @osama @mark @here", + cooked: + '', + excerpt: + '', + created_at: "2021-07-22T08:14:16.950Z", + flag_count: 0, + user: { + id: 1, + username: "markvanlan", + name: null, + avatar_template: "/letter_avatar_proxy/v4/letter/m/48db29/{size}.png", + }, + available_flags: ["spam"], +}; + +export function generateChatView(loggedInUser, metaOverrides = {}) { + const metaDefaults = { + can_flag: true, + user_silenced: false, + can_moderate: loggedInUser.staff, + can_delete_self: true, + can_delete_others: loggedInUser.staff, + }; + return { + meta: deepMerge(metaDefaults, metaOverrides), + chat_messages: [message0, message1, message2, message3], + }; +} diff --git a/plugins/chat/test/javascripts/components/chat-channel-about-view-test.js b/plugins/chat/test/javascripts/components/chat-channel-about-view-test.js new file mode 100644 index 0000000000..60daa3adb9 --- /dev/null +++ b/plugins/chat/test/javascripts/components/chat-channel-about-view-test.js @@ -0,0 +1,149 @@ +import { setupRenderingTest } from "discourse/tests/helpers/component-test"; +import { exists, query } from "discourse/tests/helpers/qunit-helpers"; +import hbs from "htmlbars-inline-precompile"; +import fabricators from "../helpers/fabricators"; +import { render, settled } from "@ember/test-helpers"; +import { module, test } from "qunit"; +import I18n from "I18n"; + +module( + "Discourse Chat | Component | chat-channel-about-view | admin user", + function (hooks) { + setupRenderingTest(hooks); + + hooks.beforeEach(function () { + this.set( + "channel", + fabricators.chatChannel({ chatable_type: "Category" }) + ); + this.channel.set("description", "foo"); + this.currentUser.set("admin", true); + this.currentUser.set("has_chat_enabled", true); + this.siteSettings.chat_enabled = true; + }); + + test("chatable name", async function (assert) { + await render(hbs`{{chat-channel-about-view channel=channel}}`); + + assert.equal( + query(".category-name").innerText, + this.channel.chatable.name + ); + }); + + test("chatable description", async function (assert) { + await render(hbs`{{chat-channel-about-view channel=channel}}`); + + assert.equal( + query(".category-name").innerText, + this.channel.chatable.name + ); + + this.channel.set("description", null); + await settled(); + + assert.equal( + query(".channel-info-about-view__description__helper-text").innerText, + I18n.t("chat.channel_edit_description_modal.description") + ); + }); + + test("edit title", async function (assert) { + await render(hbs`{{chat-channel-about-view channel=channel}}`); + + assert.ok(exists(".edit-title-btn")); + }); + + test("edit description", async function (assert) { + await render(hbs`{{chat-channel-about-view channel=channel}}`); + + assert.ok(exists(".edit-description-btn")); + }); + + test("join", async function (assert) { + await render(hbs`{{chat-channel-about-view channel=channel}}`); + + assert.ok(exists(".toggle-channel-membership-button.-join")); + }); + + test("leave", async function (assert) { + this.channel.current_user_membership.set("following", true); + await render(hbs`{{chat-channel-about-view channel=channel}}`); + + assert.ok(exists(".toggle-channel-membership-button.-leave")); + }); + } +); + +module( + "Discourse Chat | Component | chat-channel-about-view | regular user", + function (hooks) { + setupRenderingTest(hooks); + + hooks.beforeEach(function () { + this.set( + "channel", + fabricators.chatChannel({ chatable_type: "Category" }) + ); + this.channel.set("description", "foo"); + this.currentUser.set("has_chat_enabled", true); + this.siteSettings.chat_enabled = true; + }); + + test("escapes channel title", async function (assert) { + this.channel.set("title", "written text
" + + 'more written text
" + + 'and even more
"; + +const animatedImageCooked = + "written text
" + + '![]()
more written text
" + + '
and even more
"; + +const externalImageCooked = + "written text
" + + '' + + "more written text
" + + '' + + "and even more
"; + +const imageCooked = + "written text
" + + '![]()
more written text
" + + '
and even more
" + + '
written text
" + + 'more written text
"; + +const evilString = ""; +const evilStringEscaped = "<script>someeviltitle</script>"; + +module("Discourse Chat | Component | chat message collapser", function (hooks) { + setupRenderingTest(hooks); + + test("escapes uploads header", async function (assert) { + this.set("uploads", [{ original_filename: evilString }]); + await render(hbs`{{chat-message-collapser uploads=uploads}}`); + + assert.ok( + query(".chat-message-collapser-link-small").innerHTML.includes( + evilStringEscaped + ) + ); + }); +}); + +module( + "Discourse Chat | Component | chat message collapser youtube", + function (hooks) { + setupRenderingTest(hooks); + + test("escapes youtube header", async function (assert) { + this.set("cooked", youtubeCooked.replace("ytId1", evilString)); + await render(hbs`{{chat-message-collapser cooked=cooked}}`); + + assert.ok( + query(".chat-message-collapser-link").href.includes( + "%3Cscript%3Esomeeviltitle%3C/script%3E" + ) + ); + }); + + componentTest("shows youtube link in header", { + template: hbs`{{chat-message-collapser cooked=cooked}}`, + + beforeEach() { + this.set("cooked", youtubeCooked); + }, + + async test(assert) { + const link = document.querySelectorAll(".chat-message-collapser-link"); + + assert.equal(link.length, 2, "two youtube links rendered"); + assert.strictEqual( + link[0].href, + "https://www.youtube.com/watch?v=ytId1" + ); + assert.strictEqual( + link[1].href, + "https://www.youtube.com/watch?v=ytId2" + ); + }, + }); + + componentTest("shows all user written text", { + template: hbs`{{chat-message-collapser cooked=cooked}}`, + + beforeEach() { + youtubeCooked.youtubeid; + this.set("cooked", youtubeCooked); + }, + + async test(assert) { + const text = document.querySelectorAll(".chat-message-collapser p"); + + assert.equal(text.length, 3, "shows all written text"); + assert.strictEqual( + text[0].innerText, + "written text", + "first line of written text" + ); + assert.strictEqual( + text[1].innerText, + "more written text", + "third line of written text" + ); + assert.strictEqual( + text[2].innerText, + "and even more", + "fifth line of written text" + ); + }, + }); + + componentTest("collapses and expands cooked youtube", { + template: hbs`{{chat-message-collapser cooked=cooked}}`, + + beforeEach() { + this.set("cooked", youtubeCooked); + }, + + async test(assert) { + const youtubeDivs = document.querySelectorAll(".onebox"); + + assert.equal(youtubeDivs.length, 2, "two youtube previews rendered"); + + await click( + document.querySelectorAll(".chat-message-collapser-opened")[0], + "close first preview" + ); + + assert.notOk( + visible(".onebox[data-youtube-id='ytId1']"), + "first youtube preview hidden" + ); + assert.ok( + visible(".onebox[data-youtube-id='ytId2']"), + "second youtube preview still visible" + ); + + await click(".chat-message-collapser-closed"); + + assert.equal(youtubeDivs.length, 2, "two youtube previews rendered"); + + await click( + document.querySelectorAll(".chat-message-collapser-opened")[1], + "close second preview" + ); + + assert.ok( + visible(".onebox[data-youtube-id='ytId1']"), + "first youtube preview still visible" + ); + assert.notOk( + visible(".onebox[data-youtube-id='ytId2']"), + "second youtube preview hidden" + ); + + await click(".chat-message-collapser-closed"); + + assert.equal(youtubeDivs.length, 2, "two youtube previews rendered"); + }, + }); + } +); + +module( + "Discourse Chat | Component | chat message collapser images", + function (hooks) { + setupRenderingTest(hooks); + const imageTextCooked = "A picture of Tomtom
"; + + componentTest("shows filename for one image", { + template: hbs`{{chat-message-collapser cooked=cooked uploads=uploads}}`, + + beforeEach() { + this.set("cooked", imageTextCooked); + this.set("uploads", [{ original_filename: "tomtom.jpeg" }]); + }, + + async test(assert) { + assert.ok( + query(".chat-message-collapser-link-small").innerText.includes( + "tomtom.jpeg" + ) + ); + }, + }); + + componentTest("shows number of files for multiple images", { + template: hbs`{{chat-message-collapser cooked=cooked uploads=uploads}}`, + + beforeEach() { + this.set("cooked", imageTextCooked); + this.set("uploads", [{}, {}]); + }, + + async test(assert) { + assert.ok( + query(".chat-message-collapser-link-small").innerText.includes( + "2 files" + ) + ); + }, + }); + + componentTest("collapses and expands images", { + template: hbs`{{chat-message-collapser cooked=cooked uploads=uploads}}`, + + beforeEach() { + this.set("cooked", imageTextCooked); + this.set("uploads", [{ original_filename: "tomtom.png" }]); + }, + + async test(assert) { + const uploads = ".chat-uploads"; + const chatImageUpload = ".chat-img-upload"; + + assert.ok(visible(uploads)); + assert.ok(visible(chatImageUpload)); + + await click(".chat-message-collapser-opened"); + + assert.notOk(visible(uploads)); + assert.notOk(visible(chatImageUpload)); + + await click(".chat-message-collapser-closed"); + + assert.ok(visible(uploads)); + assert.ok(visible(chatImageUpload)); + }, + }); + } +); + +module( + "Discourse Chat | Component | chat message collapser animated image", + function (hooks) { + setupRenderingTest(hooks); + + componentTest("shows links for animated image", { + template: hbs`{{chat-message-collapser cooked=cooked}}`, + + beforeEach() { + this.set("cooked", animatedImageCooked); + }, + + async test(assert) { + const links = document.querySelectorAll( + "a.chat-message-collapser-link-small" + ); + + assert.ok(links[0].innerText.trim().includes("avatar.png")); + assert.ok(links[0].href.includes("avatar.png")); + + assert.ok( + links[1].innerText.trim().includes("d-logo-sketch-small.png") + ); + assert.ok(links[1].href.includes("d-logo-sketch-small.png")); + }, + }); + + componentTest("shows all user written text", { + template: hbs`{{chat-message-collapser cooked=cooked}}`, + + beforeEach() { + this.set("cooked", animatedImageCooked); + }, + + async test(assert) { + const text = document.querySelectorAll(".chat-message-collapser p"); + + assert.equal(text.length, 5, "shows all written text"); + assert.strictEqual(text[0].innerText, "written text"); + assert.strictEqual(text[2].innerText, "more written text"); + assert.strictEqual(text[4].innerText, "and even more"); + }, + }); + + componentTest("collapses and expands animated image onebox", { + template: hbs`{{chat-message-collapser cooked=cooked}}`, + + beforeEach() { + this.set("cooked", animatedImageCooked); + }, + + async test(assert) { + const animatedOneboxes = document.querySelectorAll(".animated.onebox"); + + assert.equal(animatedOneboxes.length, 2, "two oneboxes rendered"); + + await click( + document.querySelectorAll(".chat-message-collapser-opened")[0], + "close first preview" + ); + + assert.notOk( + visible(".onebox[src='/images/avatar.png']"), + "first onebox hidden" + ); + assert.ok( + visible(".onebox[src='/images/d-logo-sketch-small.png']"), + "second onebox still visible" + ); + + await click(".chat-message-collapser-closed"); + + assert.equal(animatedOneboxes.length, 2, "two oneboxes rendered"); + + await click( + document.querySelectorAll(".chat-message-collapser-opened")[1], + "close second preview" + ); + + assert.ok( + visible(".onebox[src='/images/avatar.png']"), + "first onebox still visible" + ); + assert.notOk( + visible(".onebox[src='/images/d-logo-sketch-small.png']"), + "second onebox hidden" + ); + + await click(".chat-message-collapser-closed"); + + assert.equal(animatedOneboxes.length, 2, "two oneboxes rendered"); + }, + }); + } +); + +module( + "Discourse Chat | Component | chat message collapser external image onebox", + function (hooks) { + setupRenderingTest(hooks); + + componentTest("shows links for animated image", { + template: hbs`{{chat-message-collapser cooked=cooked}}`, + + beforeEach() { + this.set("cooked", externalImageCooked); + }, + + async test(assert) { + const links = document.querySelectorAll( + "a.chat-message-collapser-link-small" + ); + + assert.ok(links[0].innerText.trim().includes("http://cat1.com")); + assert.ok(links[0].href.includes("http://cat1.com")); + + assert.ok(links[1].innerText.trim().includes("http://cat2.com")); + assert.ok(links[1].href.includes("http://cat2.com")); + }, + }); + + componentTest("shows all user written text", { + template: hbs`{{chat-message-collapser cooked=cooked}}`, + + beforeEach() { + this.set("cooked", externalImageCooked); + }, + + async test(assert) { + const text = document.querySelectorAll(".chat-message-collapser p"); + + assert.equal(text.length, 5, "shows all written text"); + assert.strictEqual(text[0].innerText, "written text"); + assert.strictEqual(text[2].innerText, "more written text"); + assert.strictEqual(text[4].innerText, "and even more"); + }, + }); + + componentTest("collapses and expands image oneboxes", { + template: hbs`{{chat-message-collapser cooked=cooked}}`, + + beforeEach() { + this.set("cooked", externalImageCooked); + }, + + async test(assert) { + const imageOneboxes = document.querySelectorAll(".onebox"); + + assert.equal(imageOneboxes.length, 2, "two oneboxes rendered"); + + await click( + document.querySelectorAll(".chat-message-collapser-opened")[0], + "close first preview" + ); + + assert.notOk( + visible(".onebox[href='http://cat1.com']"), + "first onebox hidden" + ); + assert.ok( + visible(".onebox[href='http://cat2.com']"), + "second onebox still visible" + ); + + await click(".chat-message-collapser-closed"); + + assert.equal(imageOneboxes.length, 2, "two oneboxes rendered"); + + await click( + document.querySelectorAll(".chat-message-collapser-opened")[1], + "close second preview" + ); + + assert.ok( + visible(".onebox[href='http://cat1.com']"), + "first onebox still visible" + ); + assert.notOk( + visible(".onebox[href='http://cat2.com']"), + "second onebox hidden" + ); + + await click(".chat-message-collapser-closed"); + + assert.equal(imageOneboxes.length, 2, "two oneboxes rendered"); + }, + }); + } +); + +module( + "Discourse Chat | Component | chat message collapser images", + function (hooks) { + setupRenderingTest(hooks); + + test("escapes link", async function (assert) { + this.set( + "cooked", + imageCooked + .replace("shows alt", evilString) + .replace("/images/d-logo-sketch-small.png", evilString) + ); + await render(hbs`{{chat-message-collapser cooked=cooked}}`); + + assert.ok( + queryAll(".chat-message-collapser-link-small")[0].innerHTML.includes( + evilStringEscaped + ) + ); + assert.ok( + queryAll(".chat-message-collapser-link-small")[1].innerHTML.includes( + "%3Cscript%3Esomeeviltitle%3C/script%3E" + ) + ); + }); + + componentTest("shows alt or links (if no alt) for linked image", { + template: hbs`{{chat-message-collapser cooked=cooked}}`, + + beforeEach() { + this.set("cooked", imageCooked); + }, + + async test(assert) { + const links = document.querySelectorAll( + "a.chat-message-collapser-link-small" + ); + + assert.ok(links[0].innerText.trim().includes("shows alt")); + assert.ok(links[0].href.includes("/images/avatar.png")); + + assert.ok( + links[1].innerText.trim().includes("/images/d-logo-sketch-small.png") + ); + assert.ok(links[1].href.includes("/images/d-logo-sketch-small.png")); + }, + }); + + componentTest("shows all user written text", { + template: hbs`{{chat-message-collapser cooked=cooked}}`, + + beforeEach() { + this.set("cooked", imageCooked); + }, + + async test(assert) { + const text = document.querySelectorAll(".chat-message-collapser p"); + + assert.equal(text.length, 6, "shows all written text"); + assert.strictEqual(text[0].innerText, "written text"); + assert.strictEqual(text[2].innerText, "more written text"); + assert.strictEqual(text[4].innerText, "and even more"); + }, + }); + + componentTest("collapses and expands images", { + template: hbs`{{chat-message-collapser cooked=cooked}}`, + + beforeEach() { + this.set("cooked", imageCooked); + }, + + async test(assert) { + const images = document.querySelectorAll("img"); + + assert.equal(images.length, 3); + + await click( + document.querySelectorAll(".chat-message-collapser-opened")[0], + "close first preview" + ); + + assert.notOk( + visible("img[src='/images/avatar.png']"), + "first image hidden" + ); + assert.ok( + visible("img[src='/images/d-logo-sketch-small.png']"), + "second image still visible" + ); + + await click(".chat-message-collapser-closed"); + + assert.equal(images.length, 3); + + await click( + document.querySelectorAll(".chat-message-collapser-opened")[1], + "close second preview" + ); + + assert.ok( + visible("img[src='/images/avatar.png']"), + "first image still visible" + ); + assert.notOk( + visible("img[src='/images/d-logo-sketch-small.png']"), + "second image hidden" + ); + + await click(".chat-message-collapser-closed"); + + assert.equal(images.length, 3); + }, + }); + + componentTest("does not show collapser for emoji images", { + template: hbs`{{chat-message-collapser cooked=cooked}}`, + + beforeEach() { + this.set("cooked", imageCooked); + }, + + async test(assert) { + const links = document.querySelectorAll( + "a.chat-message-collapser-link-small" + ); + const images = document.querySelectorAll("img"); + const collapser = document.querySelectorAll( + ".chat-message-collapser-opened" + ); + + assert.equal(links.length, 2); + assert.equal(images.length, 3, "shows images and emoji"); + assert.equal(collapser.length, 2); + }, + }); + } +); + +module( + "Discourse Chat | Component | chat message collapser galleries", + function (hooks) { + setupRenderingTest(hooks); + + test("escapes title/link", async function (assert) { + this.set( + "cooked", + galleryCooked + .replace("https://imgur.com/gallery/yyVx5lJ", evilString) + .replace("Le tomtom album", evilString) + ); + await render(hbs`{{chat-message-collapser cooked=cooked}}`); + + assert.ok( + query(".chat-message-collapser-link-small").href.includes( + "%3Cscript%3Esomeeviltitle%3C/script%3E" + ) + ); + assert.strictEqual( + query(".chat-message-collapser-link-small").innerHTML.trim(), + "someeviltitle" + ); + }); + + componentTest("removes album title overlay", { + template: hbs`{{chat-message-collapser cooked=cooked}}`, + + beforeEach() { + this.set("cooked", galleryCooked); + }, + + async test(assert) { + assert.notOk(visible(".album-title"), "album title removed"); + }, + }); + + componentTest("shows gallery link", { + template: hbs`{{chat-message-collapser cooked=cooked}}`, + + beforeEach() { + this.set("cooked", galleryCooked); + }, + + async test(assert) { + assert.ok( + query(".chat-message-collapser-link-small").innerText.includes( + "Le tomtom album" + ) + ); + }, + }); + + componentTest("shows all user written text", { + template: hbs`{{chat-message-collapser cooked=cooked}}`, + + beforeEach() { + this.set("cooked", galleryCooked); + }, + + async test(assert) { + const text = document.querySelectorAll(".chat-message-collapser p"); + + assert.equal(text.length, 2, "shows all written text"); + assert.strictEqual(text[0].innerText, "written text"); + assert.strictEqual(text[1].innerText, "more written text"); + }, + }); + + componentTest("collapses and expands images", { + template: hbs`{{chat-message-collapser cooked=cooked}}`, + + beforeEach() { + this.set("cooked", galleryCooked); + }, + + async test(assert) { + assert.ok(visible("img"), "image visible initially"); + + await click( + document.querySelectorAll(".chat-message-collapser-opened")[0], + "close preview" + ); + + assert.notOk(visible("img"), "image hidden"); + + await click(".chat-message-collapser-closed"); + + assert.ok(visible("img"), "image visible initially"); + }, + }); + } +); diff --git a/plugins/chat/test/javascripts/components/chat-message-info-test.js b/plugins/chat/test/javascripts/components/chat-message-info-test.js new file mode 100644 index 0000000000..ffe946fda1 --- /dev/null +++ b/plugins/chat/test/javascripts/components/chat-message-info-test.js @@ -0,0 +1,140 @@ +import Bookmark from "discourse/models/bookmark"; +import componentTest, { + setupRenderingTest, +} from "discourse/tests/helpers/component-test"; +import hbs from "htmlbars-inline-precompile"; +import { exists, query } from "discourse/tests/helpers/qunit-helpers"; +import I18n from "I18n"; +import { module } from "qunit"; +import User from "discourse/models/user"; + +module("Discourse Chat | Component | chat-message-info", function (hooks) { + setupRenderingTest(hooks); + + componentTest("chat_webhook_event", { + template: hbs`{{chat-message-info message=message}}`, + + beforeEach() { + this.set("message", { chat_webhook_event: { username: "discobot" } }); + }, + + async test(assert) { + assert.equal( + query(".chat-message-info__username").innerText.trim(), + this.message.chat_webhook_event.username + ); + assert.equal( + query(".chat-message-info__bot-indicator").textContent.trim(), + I18n.t("chat.bot") + ); + }, + }); + + componentTest("user", { + template: hbs`{{chat-message-info message=message}}`, + + beforeEach() { + this.set("message", { user: { username: "discobot" } }); + }, + + async test(assert) { + assert.equal( + query(".chat-message-info__username").innerText.trim(), + this.message.user.username + ); + }, + }); + + componentTest("date", { + template: hbs`{{chat-message-info message=message}}`, + + beforeEach() { + this.set("message", { + user: { username: "discobot" }, + created_at: moment(), + }); + }, + + async test(assert) { + assert.ok(exists(".chat-message-info__date")); + }, + }); + + componentTest("bookmark (with reminder)", { + template: hbs`{{chat-message-info message=message}}`, + + beforeEach() { + this.set("message", { + user: { username: "discobot" }, + bookmark: Bookmark.create({ + reminder_at: moment(), + name: "some name", + }), + }); + }, + + async test(assert) { + assert.ok( + exists(".chat-message-info__bookmark .d-icon-discourse-bookmark-clock") + ); + }, + }); + + componentTest("bookmark (no reminder)", { + template: hbs`{{chat-message-info message=message}}`, + + beforeEach() { + this.set("message", { + user: { username: "discobot" }, + bookmark: Bookmark.create({ + name: "some name", + }), + }); + }, + + async test(assert) { + assert.ok(exists(".chat-message-info__bookmark .d-icon-bookmark")); + }, + }); + + componentTest("user status", { + template: hbs`{{chat-message-info message=message}}`, + + beforeEach() { + const status = { description: "off to dentist", emoji: "tooth" }; + this.set("message", { user: User.create({ status }) }); + }, + + async test(assert) { + assert.ok(exists(".chat-message-info__status .user-status-message")); + }, + }); + + componentTest("reviewable", { + template: hbs`{{chat-message-info message=message}}`, + + beforeEach() { + this.set("message", { + user: { username: "discobot" }, + user_flag_status: 0, + }); + }, + + async test(assert) { + assert.equal( + query(".chat-message-info__flag > .svg-icon-title").title, + I18n.t("chat.you_flagged") + ); + + this.set("message", { + user: { username: "discobot" }, + reviewable_id: 1, + }); + + assert.equal( + query(".chat-message-info__flag a .svg-icon-title").title, + I18n.t("chat.flagged") + ); + }, + }); +}); diff --git a/plugins/chat/test/javascripts/components/chat-message-move-to-channel-modal-inner-test.js b/plugins/chat/test/javascripts/components/chat-message-move-to-channel-modal-inner-test.js new file mode 100644 index 0000000000..5763df82a8 --- /dev/null +++ b/plugins/chat/test/javascripts/components/chat-message-move-to-channel-modal-inner-test.js @@ -0,0 +1,32 @@ +import { setupRenderingTest } from "discourse/tests/helpers/component-test"; +import fabricators from "../helpers/fabricators"; +import { query } from "discourse/tests/helpers/qunit-helpers"; +import hbs from "htmlbars-inline-precompile"; +import { render } from "@ember/test-helpers"; +import { module, test } from "qunit"; + +module( + "Discourse Chat | Component | chat-message-move-to-channel-modal-inner", + function (hooks) { + setupRenderingTest(hooks); + + test("channel title is escaped in instructions correctly", async function (assert) { + this.set( + "channel", + fabricators.chatChannel({ title: "" }) + ); + this.set("chat", { publicChannels: [this.channel] }); + this.set("selectedMessageIds", [1]); + + await render( + hbs`{{chat-message-move-to-channel-modal-inner selectedMessageIds=selectedMessageIds sourceChannel=channel chat=chat}}` + ); + + assert.ok( + query(".chat-message-move-to-channel-modal-inner").innerHTML.includes( + "<script>someeviltitle</script>" + ) + ); + }); + } +); diff --git a/plugins/chat/test/javascripts/components/chat-message-reaction-test.js b/plugins/chat/test/javascripts/components/chat-message-reaction-test.js new file mode 100644 index 0000000000..9b1dd1af53 --- /dev/null +++ b/plugins/chat/test/javascripts/components/chat-message-reaction-test.js @@ -0,0 +1,104 @@ +import componentTest, { + setupRenderingTest, +} from "discourse/tests/helpers/component-test"; +import { click } from "@ember/test-helpers"; +import { exists, query } from "discourse/tests/helpers/qunit-helpers"; +import hbs from "htmlbars-inline-precompile"; +import { module } from "qunit"; + +module("Discourse Chat | Component | chat-message-reaction", function (hooks) { + setupRenderingTest(hooks); + + componentTest("accepts arbitrary class property", { + template: hbs`{{chat-message-reaction reaction=(hash emoji="heart") class="foo"}}`, + + async test(assert) { + assert.ok(exists(".chat-message-reaction.foo")); + }, + }); + + componentTest("adds reacted class when user reacted", { + template: hbs`{{chat-message-reaction reaction=(hash emoji="heart" reacted=true)}}`, + + async test(assert) { + assert.ok(exists(".chat-message-reaction.reacted")); + }, + }); + + componentTest("adds reaction name as class", { + template: hbs`{{chat-message-reaction reaction=(hash emoji="heart")}}`, + + async test(assert) { + assert.ok(exists(`.chat-message-reaction[data-emoji-name="heart"]`)); + }, + }); + + componentTest("adds show class when count is positive", { + template: hbs`{{chat-message-reaction reaction=(hash emoji="heart" count=this.count)}}`, + + beforeEach() { + this.set("count", 0); + }, + + async test(assert) { + assert.notOk(exists(".chat-message-reaction.show")); + + this.set("count", 1); + + assert.ok(exists(".chat-message-reaction.show")); + }, + }); + + componentTest("title/alt attributes", { + template: hbs`{{chat-message-reaction reaction=(hash emoji="heart")}}`, + + async test(assert) { + assert.equal(query(".chat-message-reaction").title, ":heart:"); + assert.equal(query(".chat-message-reaction img").alt, ":heart:"); + }, + }); + + componentTest("count of reactions", { + template: hbs`{{chat-message-reaction reaction=(hash emoji="heart" count=this.count)}}`, + + beforeEach() { + this.set("count", 0); + }, + + async test(assert) { + assert.notOk(exists(".chat-message-reaction .count")); + + this.set("count", 2); + + assert.equal(query(".chat-message-reaction .count").innerText, "2"); + }, + }); + + componentTest("reaction’s image", { + template: hbs`{{chat-message-reaction reaction=(hash emoji="heart")}}`, + + async test(assert) { + const src = query(".chat-message-reaction img").src; + assert.ok(/heart\.png/.test(src)); + }, + }); + + componentTest("click action", { + template: hbs`{{chat-message-reaction class="show" reaction=(hash emoji="heart" count=this.count) react=this.react}}`, + + beforeEach() { + this.set("count", 0); + this.set("react", () => { + this.set("count", 1); + }); + }, + + async test(assert) { + assert.notOk(exists(".chat-message-reaction .count")); + + await click(".chat-message-reaction"); + + assert.equal(query(".chat-message-reaction .count").innerText, "1"); + }, + }); +}); diff --git a/plugins/chat/test/javascripts/components/chat-message-separator-test.js b/plugins/chat/test/javascripts/components/chat-message-separator-test.js new file mode 100644 index 0000000000..6282130718 --- /dev/null +++ b/plugins/chat/test/javascripts/components/chat-message-separator-test.js @@ -0,0 +1,44 @@ +import componentTest, { + setupRenderingTest, +} from "discourse/tests/helpers/component-test"; +import { query } from "discourse/tests/helpers/qunit-helpers"; +import hbs from "htmlbars-inline-precompile"; +import I18n from "I18n"; +import { module } from "qunit"; + +module("Discourse Chat | Component | chat-message-separator", function (hooks) { + setupRenderingTest(hooks); + + componentTest("newest message", { + template: hbs`{{chat-message-separator message=message}}`, + + async beforeEach() { + this.set("message", { newestMessage: true }); + }, + + async test(assert) { + assert.equal( + query(".chat-message-separator.new-message .text").innerText.trim(), + I18n.t("chat.new_messages") + ); + }, + }); + + componentTest("first message of the day", { + template: hbs`{{chat-message-separator message=message}}`, + + async beforeEach() { + this.set("date", moment().format("LLL")); + this.set("message", { firstMessageOfTheDayAt: this.date }); + }, + + async test(assert) { + assert.equal( + query( + ".chat-message-separator.first-daily-message .text" + ).innerText.trim(), + this.date + ); + }, + }); +}); diff --git a/plugins/chat/test/javascripts/components/chat-message-test.js b/plugins/chat/test/javascripts/components/chat-message-test.js new file mode 100644 index 0000000000..a937d66828 --- /dev/null +++ b/plugins/chat/test/javascripts/components/chat-message-test.js @@ -0,0 +1,117 @@ +import User from "discourse/models/user"; +import { render, waitFor } from "@ember/test-helpers"; +import ChatMessage from "discourse/plugins/chat/discourse/models/chat-message"; +import { exists } from "discourse/tests/helpers/qunit-helpers"; +import { setupRenderingTest } from "discourse/tests/helpers/component-test"; +import hbs from "htmlbars-inline-precompile"; +import ChatChannel from "discourse/plugins/chat/discourse/models/chat-channel"; +import { module, test } from "qunit"; + +module("Discourse Chat | Component | chat-message", function (hooks) { + setupRenderingTest(hooks); + + function generateMessageProps(messageData = {}) { + const chatChannel = ChatChannel.create({ + chatable: { id: 1 }, + chatable_type: "Category", + id: 9, + title: "Site", + last_message_sent_at: "2021-11-08T21:26:05.710Z", + current_user_membership: { + unread_count: 0, + muted: false, + }, + }); + return { + message: ChatMessage.create( + Object.assign( + { + id: 178, + message: "from deleted user", + cooked: "from deleted user
", + excerpt: "from deleted user
", + created_at: "2021-07-22T08:14:16.950Z", + flag_count: 0, + user: User.create({ username: "someguy", id: 1424 }), + edited: false, + }, + messageData + ) + ), + canInteractWithChat: true, + details: { + can_delete_self: true, + can_delete_others: true, + can_flag: true, + user_silenced: false, + can_moderate: true, + }, + chatChannel, + setReplyTo: () => {}, + replyMessageClicked: () => {}, + editButtonClicked: () => {}, + afterExpand: () => {}, + selectingMessages: false, + onStartSelectingMessages: () => {}, + onSelectMessage: () => {}, + bulkSelectMessages: () => {}, + afterReactionAdded: () => {}, + onHoverMessage: () => {}, + }; + } + + const template = hbs`{{chat-message + message=message + canInteractWithChat=canInteractWithChat + details=this.details + chatChannel=chatChannel + setReplyTo=setReplyTo + replyMessageClicked=replyMessageClicked + editButtonClicked=editButtonClicked + selectingMessages=selectingMessages + onStartSelectingMessages=onStartSelectingMessages + onSelectMessage=onSelectMessage + bulkSelectMessages=bulkSelectMessages + onHoverMessage=onHoverMessage + afterReactionAdded=reStickScrollIfNeeded + }}`; + + test("Message with edits", async function (assert) { + this.setProperties(generateMessageProps({ edited: true })); + await render(template); + assert.ok( + exists(".chat-message-edited"), + "has the correct edited css class" + ); + }); + + test("Deleted message", async function (assert) { + this.setProperties(generateMessageProps({ deleted_at: moment() })); + await render(template); + assert.ok( + exists(".chat-message-deleted .chat-message-expand"), + "has the correct deleted css class and expand button within" + ); + }); + + test("Hidden message", async function (assert) { + this.setProperties(generateMessageProps({ hidden: true })); + await render(template); + assert.ok( + exists(".chat-message-hidden .chat-message-expand"), + "has the correct hidden css class and expand button within" + ); + }); + + test("Message marked as visible", async function (assert) { + this.setProperties(generateMessageProps()); + + await render(template); + await waitFor("div[data-visible=true]"); + + assert.ok( + exists(".chat-message-container[data-visible=true]"), + "message is marked as visible" + ); + }); +}); diff --git a/plugins/chat/test/javascripts/components/chat-message-text-test.js b/plugins/chat/test/javascripts/components/chat-message-text-test.js new file mode 100644 index 0000000000..938cb5bc76 --- /dev/null +++ b/plugins/chat/test/javascripts/components/chat-message-text-test.js @@ -0,0 +1,76 @@ +import componentTest, { + setupRenderingTest, +} from "discourse/tests/helpers/component-test"; +import hbs from "htmlbars-inline-precompile"; +import { exists } from "discourse/tests/helpers/qunit-helpers"; +import { module } from "qunit"; + +module("Discourse Chat | Component | chat-message-text", function (hooks) { + setupRenderingTest(hooks); + + componentTest("yields", { + template: hbs`{{#chat-message-text cooked=cooked uploads=uploads}} {{/chat-message-text}}`, + + beforeEach() { + this.set("cooked", ""); + }, + + async test(assert) { + assert.ok(exists(".yield-me")); + }, + }); + + componentTest("shows collapsed", { + template: hbs`{{chat-message-text cooked=cooked uploads=uploads}}`, + + beforeEach() { + this.set( + "cooked", + '' + ); + }, + + async test(assert) { + assert.ok(exists(".chat-message-collapser")); + }, + }); + + componentTest("does not collapse a non-image onebox", { + template: hbs`{{chat-message-text cooked=cooked}}`, + + beforeEach() { + this.set( + "cooked", + '' + ); + }, + + async test(assert) { + assert.notOk(exists(".chat-message-collapser")); + }, + }); + + componentTest("shows edits - regular message", { + template: hbs`{{chat-message-text cooked=cooked edited=true}}`, + + beforeEach() { + this.set("cooked", ""); + }, + + async test(assert) { + assert.ok(exists(".chat-message-edited")); + }, + }); + + componentTest("shows edits - collapsible message", { + template: hbs`{{chat-message-text cooked=cooked edited=true}}`, + + beforeEach() { + this.set("cooked", ''); + }, + + async test(assert) { + assert.ok(exists(".chat-message-edited")); + }, + }); +}); diff --git a/plugins/chat/test/javascripts/components/chat-replying-indicator-test.js b/plugins/chat/test/javascripts/components/chat-replying-indicator-test.js new file mode 100644 index 0000000000..84468a68b1 --- /dev/null +++ b/plugins/chat/test/javascripts/components/chat-replying-indicator-test.js @@ -0,0 +1,182 @@ +import componentTest, { + setupRenderingTest, +} from "discourse/tests/helpers/component-test"; +import { exists, query } from "discourse/tests/helpers/qunit-helpers"; +import hbs from "htmlbars-inline-precompile"; +import fabricators from "../helpers/fabricators"; +import MockPresenceChannel from "../helpers/mock-presence-channel"; +import { module } from "qunit"; + +module( + "Discourse Chat | Component | chat-replying-indicator", + function (hooks) { + setupRenderingTest(hooks); + + componentTest("not displayed when no one is replying", { + template: hbs`{{chat-replying-indicator presenceChannel=presenceChannel chatChannel=chatChannel}}`, + + async beforeEach() { + this.set("chatChannel", fabricators.chatChannel()); + this.set( + "presenceChannel", + MockPresenceChannel.create({ + name: `/chat-reply/${this.chatChannel.id}`, + }) + ); + }, + + async test(assert) { + assert.notOk(exists(".chat-replying-indicator__text")); + }, + }); + + componentTest("displays indicator when user is replying", { + template: hbs`{{chat-replying-indicator presenceChannel=presenceChannel chatChannel=chatChannel}}`, + + async beforeEach() { + this.set("chatChannel", fabricators.chatChannel()); + this.set( + "presenceChannel", + MockPresenceChannel.create({ + name: `/chat-reply/${this.chatChannel.id}`, + }) + ); + }, + + async test(assert) { + const sam = { id: 1, username: "sam" }; + this.set("presenceChannel.users", [sam]); + + assert.equal( + query(".chat-replying-indicator__text").innerText, + `${sam.username} is typing` + ); + }, + }); + + componentTest("displays indicator when 2 or 3 users are replying", { + template: hbs`{{chat-replying-indicator presenceChannel=presenceChannel chatChannel=chatChannel}}`, + + async beforeEach() { + this.set("chatChannel", fabricators.chatChannel()); + this.set( + "presenceChannel", + MockPresenceChannel.create({ + name: `/chat-reply/${this.chatChannel.id}`, + }) + ); + }, + + async test(assert) { + const sam = { id: 1, username: "sam" }; + const mark = { id: 2, username: "mark" }; + this.set("presenceChannel.users", [sam, mark]); + + assert.equal( + query(".chat-replying-indicator__text").innerText, + `${sam.username} and ${mark.username} are typing` + ); + }, + }); + + componentTest("displays indicator when 3 users are replying", { + template: hbs`{{chat-replying-indicator presenceChannel=presenceChannel chatChannel=chatChannel}}`, + + async beforeEach() { + this.set("chatChannel", fabricators.chatChannel()); + this.set( + "presenceChannel", + MockPresenceChannel.create({ + name: `/chat-reply/${this.chatChannel.id}`, + }) + ); + }, + + async test(assert) { + const sam = { id: 1, username: "sam" }; + const mark = { id: 2, username: "mark" }; + const joffrey = { id: 3, username: "joffrey" }; + this.set("presenceChannel.users", [sam, mark, joffrey]); + + assert.equal( + query(".chat-replying-indicator__text").innerText, + `${sam.username}, ${mark.username} and ${joffrey.username} are typing` + ); + }, + }); + + componentTest("displays indicator when more than 3 users are replying", { + template: hbs`{{chat-replying-indicator presenceChannel=presenceChannel chatChannel=chatChannel}}`, + + async beforeEach() { + this.set("chatChannel", fabricators.chatChannel()); + this.set( + "presenceChannel", + MockPresenceChannel.create({ + name: `/chat-reply/${this.chatChannel.id}`, + }) + ); + }, + + async test(assert) { + const sam = { id: 1, username: "sam" }; + const mark = { id: 2, username: "mark" }; + const joffrey = { id: 3, username: "joffrey" }; + const taylor = { id: 4, username: "taylor" }; + this.set("presenceChannel.users", [sam, mark, joffrey, taylor]); + + assert.equal( + query(".chat-replying-indicator__text").innerText, + `${sam.username}, ${mark.username} and 2 others are typing` + ); + }, + }); + + componentTest("filters current user from list of repliers", { + template: hbs`{{chat-replying-indicator presenceChannel=presenceChannel chatChannel=chatChannel}}`, + + async beforeEach() { + this.set("chatChannel", fabricators.chatChannel()); + this.set( + "presenceChannel", + MockPresenceChannel.create({ + name: `/chat-reply/${this.chatChannel.id}`, + }) + ); + }, + + async test(assert) { + const sam = { id: 1, username: "sam" }; + this.set("presenceChannel.users", [sam, this.currentUser]); + + assert.equal( + query(".chat-replying-indicator__text").innerText, + `${sam.username} is typing` + ); + }, + }); + + componentTest("resets presence when channel is draft", { + template: hbs`{{chat-replying-indicator presenceChannel=presenceChannel chatChannel=chatChannel}}`, + + async beforeEach() { + this.set("chatChannel", fabricators.chatChannel()); + this.set( + "presenceChannel", + MockPresenceChannel.create({ + name: `/chat-reply/${this.chatChannel.id}`, + subscribed: true, + }) + ); + }, + + async test(assert) { + assert.ok(this.presenceChannel.subscribed); + + this.set("chatChannel", fabricators.chatChannel({ isDraft: true })); + + assert.notOk(this.presenceChannel.subscribed); + }, + }); + } +); diff --git a/plugins/chat/test/javascripts/components/chat-retention-reminder-test.js b/plugins/chat/test/javascripts/components/chat-retention-reminder-test.js new file mode 100644 index 0000000000..89620f1c93 --- /dev/null +++ b/plugins/chat/test/javascripts/components/chat-retention-reminder-test.js @@ -0,0 +1,93 @@ +import ChatChannel from "discourse/plugins/chat/discourse/models/chat-channel"; +import { set } from "@ember/object"; +import componentTest, { + setupRenderingTest, +} from "discourse/tests/helpers/component-test"; +import { exists, query } from "discourse/tests/helpers/qunit-helpers"; +import hbs from "htmlbars-inline-precompile"; +import I18n from "I18n"; +import { module } from "qunit"; + +module( + "Discourse Chat | Component | chat-retention-reminder", + function (hooks) { + setupRenderingTest(hooks); + + componentTest("Shows for public channels when user needs it", { + template: hbs`{{chat-retention-reminder chatChannel=chatChannel}}`, + + async beforeEach() { + this.set( + "chatChannel", + ChatChannel.create({ chatable_type: "Category" }) + ); + set(this.currentUser, "needs_channel_retention_reminder", true); + this.siteSettings.chat_channel_retention_days = 100; + }, + + async test(assert) { + assert.equal( + query(".chat-retention-reminder-text").innerText.trim(), + I18n.t("chat.retention_reminders.public", { days: 100 }) + ); + }, + }); + + componentTest( + "Doesn't show for public channels when user has dismissed it", + { + template: hbs`{{chat-retention-reminder chatChannel=chatChannel}}`, + + async beforeEach() { + this.set( + "chatChannel", + ChatChannel.create({ chatable_type: "Category" }) + ); + set(this.currentUser, "needs_channel_retention_reminder", false); + this.siteSettings.chat_channel_retention_days = 100; + }, + + async test(assert) { + assert.notOk(exists(".chat-retention-reminder")); + }, + } + ); + + componentTest("Shows for direct message channels when user needs it", { + template: hbs`{{chat-retention-reminder chatChannel=chatChannel}}`, + + async beforeEach() { + this.set( + "chatChannel", + ChatChannel.create({ chatable_type: "DirectMessage" }) + ); + set(this.currentUser, "needs_dm_retention_reminder", true); + this.siteSettings.chat_dm_retention_days = 100; + }, + + async test(assert) { + assert.equal( + query(".chat-retention-reminder-text").innerText.trim(), + I18n.t("chat.retention_reminders.dm", { days: 100 }) + ); + }, + }); + + componentTest("Doesn't show for dm channels when user has dismissed it", { + template: hbs`{{chat-retention-reminder chatChannel=chatChannel}}`, + + async beforeEach() { + this.set( + "chatChannel", + ChatChannel.create({ chatable_type: "DirectMessage" }) + ); + set(this.currentUser, "needs_dm_retention_reminder", false); + this.siteSettings.chat_dm_retention_days = 100; + }, + + async test(assert) { + assert.notOk(exists(".chat-retention-reminder")); + }, + }); + } +); diff --git a/plugins/chat/test/javascripts/components/chat-upload-test.js b/plugins/chat/test/javascripts/components/chat-upload-test.js new file mode 100644 index 0000000000..989da1f8be --- /dev/null +++ b/plugins/chat/test/javascripts/components/chat-upload-test.js @@ -0,0 +1,118 @@ +import componentTest, { + setupRenderingTest, +} from "discourse/tests/helpers/component-test"; +import { exists, query } from "discourse/tests/helpers/qunit-helpers"; +import hbs from "htmlbars-inline-precompile"; +import { module } from "qunit"; +import { settled } from "@ember/test-helpers"; + +const IMAGE_FIXTURE = { + id: 290, + url: null, // Nulled out to avoid actually setting the img src - avoids an HTTP request + original_filename: "image.jpg", + filesize: 172214, + width: 1024, + height: 768, + thumbnail_width: 666, + thumbnail_height: 500, + extension: "jpeg", + short_url: "upload://mnCnqY5tunCFw2qMgtPnu1mu1C9.jpeg", + short_path: "/uploads/short-url/mnCnqY5tunCFw2qMgtPnu1mu1C9.jpeg", + retain_hours: null, + human_filesize: "168 KB", + dominant_color: "788370", // rgb(120, 131, 112) +}; + +const VIDEO_FIXTURE = { + id: 290, + url: null, // Nulled out to avoid actually setting the img src - avoids an HTTP request + original_filename: "video.mp4", + filesize: 172214, + width: 1024, + height: 768, + thumbnail_width: 666, + thumbnail_height: 500, + extension: "mp4", + short_url: "upload://mnCnqY5tunCFw2qMgtPnu1mu1C9.mp4", + short_path: "/uploads/short-url/mnCnqY5tunCFw2qMgtPnu1mu1C9.mp4", + retain_hours: null, + human_filesize: "168 KB", +}; + +const TXT_FIXTURE = { + id: 290, + url: "https://example.com/file.txt", + original_filename: "file.txt", + filesize: 172214, + extension: "txt", + short_url: "upload://mnCnqY5tunCFw2qMgtPnu1mu1C9.jpeg", + short_path: "/uploads/short-url/mnCnqY5tunCFw2qMgtPnu1mu1C9.jpeg", + retain_hours: null, + human_filesize: "168 KB", +}; + +module("Discourse Chat | Component | chat-upload", function (hooks) { + setupRenderingTest(hooks); + + componentTest("with an image", { + template: hbs`{{chat-upload upload=upload}}`, + + beforeEach() { + this.set("upload", IMAGE_FIXTURE); + }, + + async test(assert) { + assert.true(exists("img.chat-img-upload"), "displays as an image"); + const image = query("img.chat-img-upload"); + assert.strictEqual(image.loading, "lazy", "is lazy loading"); + + assert.strictEqual( + image.style.backgroundColor, + "rgb(120, 131, 112)", + "sets background to dominant color" + ); + + image.dispatchEvent(new Event("load")); // Fake that the image has loaded + await settled(); + + assert.strictEqual( + image.style.backgroundColor, + "", + "removes the background color once the image has loaded" + ); + }, + }); + + componentTest("with a video", { + template: hbs`{{chat-upload upload=upload}}`, + + beforeEach() { + this.set("upload", VIDEO_FIXTURE); + }, + + async test(assert) { + assert.true(exists("video.chat-video-upload"), "displays as an video"); + const video = query("video.chat-video-upload"); + assert.ok(video.hasAttribute("controls"), "has video controls"); + assert.strictEqual( + video.getAttribute("preload"), + "metadata", + "video has correct preload settings" + ); + }, + }); + + componentTest("non image upload", { + template: hbs`{{chat-upload upload=upload}}`, + + beforeEach() { + this.set("upload", TXT_FIXTURE); + }, + + async test(assert) { + assert.true(exists("a.chat-other-upload"), "displays as a link"); + const link = query("a.chat-other-upload"); + assert.strictEqual(link.href, TXT_FIXTURE.url, "has the correct URL"); + }, + }); +}); diff --git a/plugins/chat/test/javascripts/components/chat-user-avatar-test.js b/plugins/chat/test/javascripts/components/chat-user-avatar-test.js new file mode 100644 index 0000000000..3bed00c31e --- /dev/null +++ b/plugins/chat/test/javascripts/components/chat-user-avatar-test.js @@ -0,0 +1,55 @@ +import componentTest, { + setupRenderingTest, +} from "discourse/tests/helpers/component-test"; +import { exists } from "discourse/tests/helpers/qunit-helpers"; +import hbs from "htmlbars-inline-precompile"; +import { module } from "qunit"; + +const user = { + id: 1, + username: "markvanlan", + name: null, + avatar_template: "/letter_avatar_proxy/v4/letter/m/48db29/{size}.png", +}; + +module("Discourse Chat | Component | chat-user-avatar", function (hooks) { + setupRenderingTest(hooks); + + componentTest("user is not online", { + template: hbs`{{chat-user-avatar chat=chat user=user}}`, + + async beforeEach() { + this.set("user", user); + this.set("chat", { presenceChannel: { users: [] } }); + }, + + async test(assert) { + assert.ok( + exists( + `.chat-user-avatar .chat-user-avatar-container[data-user-card=${user.username}] .avatar[title=${user.username}]` + ) + ); + assert.notOk(exists(".chat-user-avatar.is-online")); + }, + }); + + componentTest("user is online", { + template: hbs`{{chat-user-avatar chat=chat user=user}}`, + + async beforeEach() { + this.set("user", user); + this.set("chat", { + presenceChannel: { users: [{ id: user.id }] }, + }); + }, + + async test(assert) { + assert.ok( + exists( + `.chat-user-avatar .chat-user-avatar-container[data-user-card=${user.username}] .avatar[title=${user.username}]` + ) + ); + assert.ok(exists(".chat-user-avatar.is-online")); + }, + }); +}); diff --git a/plugins/chat/test/javascripts/components/chat-user-display-name-test.js b/plugins/chat/test/javascripts/components/chat-user-display-name-test.js new file mode 100644 index 0000000000..26e56d2053 --- /dev/null +++ b/plugins/chat/test/javascripts/components/chat-user-display-name-test.js @@ -0,0 +1,76 @@ +import componentTest, { + setupRenderingTest, +} from "discourse/tests/helpers/component-test"; +import { query } from "discourse/tests/helpers/qunit-helpers"; +import hbs from "htmlbars-inline-precompile"; +import { module } from "qunit"; + +function displayName() { + return query(".chat-user-display-name").innerText.trim(); +} + +module( + "Discourse Chat | Component | chat-user-display-name | prioritize username in UX", + function (hooks) { + setupRenderingTest(hooks); + + componentTest("username and no name", { + template: hbs`{{chat-user-display-name user=user}}`, + + async beforeEach() { + this.siteSettings.prioritize_username_in_ux = true; + this.set("user", { username: "bob", name: null }); + }, + + async test(assert) { + assert.equal(displayName(), "bob"); + }, + }); + + componentTest("username and name", { + template: hbs`{{chat-user-display-name user=user}}`, + + async beforeEach() { + this.siteSettings.prioritize_username_in_ux = true; + this.set("user", { username: "bob", name: "Bobcat" }); + }, + + async test(assert) { + assert.equal(displayName(), "bob — Bobcat"); + }, + }); + } +); + +module( + "Discourse Chat | Component | chat-user-display-name | prioritize name in UX", + function (hooks) { + setupRenderingTest(hooks); + + componentTest("no name", { + template: hbs`{{chat-user-display-name user=user}}`, + + async beforeEach() { + this.siteSettings.prioritize_username_in_ux = false; + this.set("user", { username: "bob", name: null }); + }, + + async test(assert) { + assert.equal(displayName(), "bob"); + }, + }); + + componentTest("name and username", { + template: hbs`{{chat-user-display-name user=user}}`, + + async beforeEach() { + this.siteSettings.prioritize_username_in_ux = false; + this.set("user", { username: "bob", name: "Bobcat" }); + }, + + async test(assert) { + assert.equal(displayName(), "Bobcat — bob"); + }, + }); + } +); diff --git a/plugins/chat/test/javascripts/components/collapser-test.js b/plugins/chat/test/javascripts/components/collapser-test.js new file mode 100644 index 0000000000..8409c97a99 --- /dev/null +++ b/plugins/chat/test/javascripts/components/collapser-test.js @@ -0,0 +1,45 @@ +import componentTest, { + setupRenderingTest, +} from "discourse/tests/helpers/component-test"; +import { click } from "@ember/test-helpers"; +import hbs from "htmlbars-inline-precompile"; +import { exists, query, visible } from "discourse/tests/helpers/qunit-helpers"; +import { module } from "qunit"; +import { htmlSafe } from "@ember/template"; + +module("Discourse Chat | Component | collapser", function (hooks) { + setupRenderingTest(hooks); + + componentTest("renders header", { + template: hbs`{{collapser header=header}}`, + + beforeEach() { + this.set("header", htmlSafe("U kunt die bron niet bekijken!
+Je kunt die bron niet bekijken!
Dit zal worden vervangen door een eigen Discourse 403-pagina.
Misschien probeerde u iets te wijzigen waarvoor u geen toegang hebt.
+Misschien probeerde je iets te wijzigen waartoe je geen toegang hebt.
De software die dit discussieforum mogelijk maakt, ondervond een onverwacht probleem. Onze excuses voor het ongemak.
Er is gedetailleerde informatie over de fout vastgelegd, en een automatische melding gegenereerd. We zullen ernaar kijken.
-Er is geen verdere actie nodig. Als de foutsituatie echter blijft bestaan, kunt u extra details verstrekken, waaronder stappen om de fout te reproduceren, door een discussietopic in de feedbackcategorie van de website te openen.
+Er is geen verdere actie nodig. Als de foutsituatie echter blijft bestaan, kun je aanvullende informatie verstrekken, waaronder stappen om de fout te reproduceren, door een discussietopic te openen in de feedbackcategorie van de website.