nodebb-plugin-poll 4.0.14 → 4.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,47 +1,47 @@
1
- {
2
- "poll": "Anket",
3
- "toggles": "Değişiklikler",
4
- "allow_guests": "Ziyaretçilerin anket sonucunu görmelerine izin ver",
5
- "limits": "Sınırlamalar",
6
- "max_options": "Anketlerdeki maksimum seçenek sayısı",
7
- "defaults": "Varsayılanlar",
8
- "default_title": "Varsayılan Anket Başlığı",
9
- "max_votes": "Kişi başı maksimum oy sayısı",
10
- "disallow_vote_update": "Kişilerin oylarını değiştirmelerine izin verme",
11
- "info_choices": "1'den büyük bir sayı çoktan seçmeli anket oluşturacak.",
12
- "settings": "Ayarlar",
13
- "save": "Kaydet",
14
- "reset": "Sıfırla",
15
-
16
- "creator_title": "Anket Oluştur",
17
- "poll_title": "Anket Başlığı",
18
- "poll_title_placeholder": "Anket başlığını buraya girin",
19
- "options_title": "Seçenekler",
20
- "options_add": "Seçenek ekle",
21
- "max_votes_placeholder": "Bir sayı girin",
22
- "auto_end_title": "Anketi otomatik olarak bitir",
23
- "auto_end_placeholder": "Tarih ve saat bilgisini buraya girin",
24
- "auto_end_help": "Burayı boş bırakırsanız anket hiç sonlanmayacak.",
25
-
26
- "error.max_options": "Sadece %d seçenek oluşturabilirsiniz.",
27
- "error.no_options": "En az bir seçenek oluşturmalısınız",
28
- "error.valid_date": "Lütfen geçerli bir tarih girin",
29
-
30
- "error.not_main": "Anketi sadece ilk iletiye ekleyebilirsiniz.",
31
- "error.privilege.create": "Anket oluşturmaya yetkiniz yok",
32
-
33
- "warning.redactor": "Redactor kullanıyorsunuz. Markup'ı manuel olarak <strong>değiştirmeyin</strong>. Bunun yerine anket oluşturucuyu yeniden açın.",
34
-
35
- "vote": "Oy ver",
36
- "update_vote": "Oyu güncelle",
37
- "remove_vote": "Oyu kaldır",
38
- "to_voting": "Seçeneklere geri dön",
39
- "to_results": "Sonuçlar",
40
- "voting_unavailable_title": "Oy vermek artık mümkün değil",
41
- "voting_unavailable_message": "Bu anket bitti veya silindi. Sonuçları hala görmeniz mümkün",
42
- "voting_update_disallowed_message": "Daha önce oy verdiniz. Bu anket için oyunuzu değiştiremezsiniz",
43
- "vote_is_final": "Bu ankette oy değiştirmeniz mümkün değildir, o nedenle seçiminiz son karardır",
44
-
45
- "vote_count": "kişi bu seçenek için oy kullandı",
46
- "votes": "oy"
47
- }
1
+ {
2
+ "poll": "Anket",
3
+ "toggles": "Değişiklikler",
4
+ "allow_guests": "Ziyaretçilerin anket sonucunu görmelerine izin ver",
5
+ "limits": "Sınırlamalar",
6
+ "max_options": "Anketlerdeki maksimum seçenek sayısı",
7
+ "defaults": "Varsayılanlar",
8
+ "default_title": "Varsayılan Anket Başlığı",
9
+ "max_votes": "Kişi başı maksimum oy sayısı",
10
+ "disallow_vote_update": "Kişilerin oylarını değiştirmelerine izin verme",
11
+ "info_choices": "1'den büyük bir sayı çoktan seçmeli anket oluşturacak.",
12
+ "settings": "Ayarlar",
13
+ "save": "Kaydet",
14
+ "reset": "Sıfırla",
15
+
16
+ "creator_title": "Anket Oluştur",
17
+ "poll_title": "Anket Başlığı",
18
+ "poll_title_placeholder": "Anket başlığını buraya girin",
19
+ "options_title": "Seçenekler",
20
+ "options_add": "Seçenek ekle",
21
+ "max_votes_placeholder": "Bir sayı girin",
22
+ "auto_end_title": "Anketi otomatik olarak bitir",
23
+ "auto_end_placeholder": "Tarih ve saat bilgisini buraya girin",
24
+ "auto_end_help": "Burayı boş bırakırsanız anket hiç sonlanmayacak.",
25
+
26
+ "error.max_options": "Sadece %1 seçenek oluşturabilirsiniz.",
27
+ "error.no_options": "En az bir seçenek oluşturmalısınız",
28
+ "error.valid_date": "Lütfen geçerli bir tarih girin",
29
+
30
+ "error.not_main": "Anketi sadece ilk iletiye ekleyebilirsiniz.",
31
+ "error.privilege.create": "Anket oluşturmaya yetkiniz yok",
32
+
33
+ "warning.redactor": "Redactor kullanıyorsunuz. Markup'ı manuel olarak <strong>değiştirmeyin</strong>. Bunun yerine anket oluşturucuyu yeniden açın.",
34
+
35
+ "vote": "Oy ver",
36
+ "update_vote": "Oyu güncelle",
37
+ "remove_vote": "Oyu kaldır",
38
+ "to_voting": "Seçeneklere geri dön",
39
+ "to_results": "Sonuçlar",
40
+ "voting_unavailable_title": "Oy vermek artık mümkün değil",
41
+ "voting_unavailable_message": "Bu anket bitti veya silindi. Sonuçları hala görmeniz mümkün",
42
+ "voting_update_disallowed_message": "Daha önce oy verdiniz. Bu anket için oyunuzu değiştiremezsiniz",
43
+ "vote_is_final": "Bu ankette oy değiştirmeniz mümkün değildir, o nedenle seçiminiz son karardır",
44
+
45
+ "vote_count": "kişi bu seçenek için oy kullandı",
46
+ "votes": "oy"
47
+ }
package/lib/config.js CHANGED
@@ -28,9 +28,20 @@ Config.defaults = {
28
28
 
29
29
  Config.getSettings = async function () {
30
30
  const settings = await meta.settings.get(Config.plugin.id);
31
- return {
31
+ const merged = {
32
32
  ...Config.defaults,
33
33
  ...settings,
34
34
  };
35
+ // NodeBB's settings helper stores checkboxes as 'on'/'off'; normalize to a boolean
36
+ // so parseInt(...) === 1 checks don't silently fail.
37
+ merged.allowGuestsToViewResults = ['1', 1, 'on', true].includes(merged.allowGuestsToViewResults);
38
+ // NodeBB's settings helper stores blank number fields as ''; fall back to defaults
39
+ // so an empty ACP field doesn't override the configured default.
40
+ merged.maxOptions = parseInt(merged.maxOptions, 10) || Config.defaults.maxOptions;
41
+ merged.maximumVotesPerUser = parseInt(merged.maximumVotesPerUser, 10) || Config.defaults.maximumVotesPerUser;
42
+ // NodeBB's settings helper stores blank text fields as ''; fall back to the
43
+ // configured default so an empty ACP field doesn't override the built-in default.
44
+ merged.defaultTitle = merged.defaultTitle || Config.defaults.defaultTitle;
45
+ return merged;
35
46
  };
36
47
 
package/lib/hooks.js CHANGED
@@ -44,7 +44,6 @@ Hooks.filter.postCreate = async function (hookData) {
44
44
  // post is going to be saved to db, data is what is submitted by user
45
45
  const { post, data } = hookData;
46
46
  if (Array.isArray(data?.polls) && data.polls.length) {
47
- await checkMaxOptions(data.polls);
48
47
  const savedPolls = await Poll.add(post, data.polls);
49
48
  if (savedPolls.length) {
50
49
  post.pollIds = JSON.stringify(savedPolls.map(p => String(p.pollId)));
@@ -74,7 +73,6 @@ Hooks.filter.postEdit = async function (hookData) {
74
73
  return hookData;
75
74
  }
76
75
  const pollData = hookData.data.polls || [];
77
- await checkMaxOptions(pollData);
78
76
  const toAdd = pollData.filter(p => !currentPollIds.includes(String(p.pollId)));
79
77
 
80
78
  if (toAdd.length) {
@@ -136,15 +134,3 @@ async function canCreate(cid, uid) {
136
134
  }
137
135
  }
138
136
 
139
- async function checkMaxOptions(polls) {
140
- if (!Array.isArray(polls) || !polls.length) {
141
- return;
142
- }
143
- const { maxOptions } = await Config.getSettings();
144
- polls.forEach((poll) => {
145
- if (poll && poll.options.length > maxOptions) {
146
- throw new Error(`[[poll:error.max_options, ${maxOptions}]]`);
147
- }
148
- });
149
- }
150
-
package/lib/poll.js CHANGED
@@ -3,6 +3,7 @@
3
3
  const db = nodebb.require('./src/database');
4
4
  const privileges = nodebb.require('./src/privileges');
5
5
 
6
+ const Config = require('./config');
6
7
  const Vote = require('./vote');
7
8
 
8
9
  const Poll = exports;
@@ -10,6 +11,7 @@ const Poll = exports;
10
11
  Poll.add = async function (postData, polls) {
11
12
  // postData is what is being saved to db
12
13
  // polls is submited by user
14
+ await validateMaxOptions(polls);
13
15
  const savePolls = [];
14
16
  for (const pollData of polls) {
15
17
  // eslint-disable-next-line no-await-in-loop
@@ -75,6 +77,9 @@ Poll.edit = async function (postData, newPolls) {
75
77
  const toDelete = currentPollIds.filter(pollId => !newPollIds.includes(String(pollId)));
76
78
  const currentPolls = await db.getObjects(currentPollIds.map(pollId => `poll:${pollId}`));
77
79
 
80
+ await validateMaxOptions(toAdd);
81
+ await validateMaxOptionsForEdit(toUpdate, currentPolls);
82
+
78
83
  await Poll.deletePolls(toDelete);
79
84
 
80
85
  const savedPolls = await Poll.add(postData, toAdd);
@@ -245,6 +250,46 @@ function tryParseOptions(optionsJson) {
245
250
  }
246
251
  }
247
252
 
253
+ async function validateMaxOptions(polls) {
254
+ if (!Array.isArray(polls) || !polls.length) {
255
+ return;
256
+ }
257
+ const settings = await Config.getSettings();
258
+ const { maxOptions } = settings;
259
+ polls.forEach((poll) => {
260
+ if (!poll) {
261
+ return;
262
+ }
263
+ const optionCount = Array.isArray(poll.options) ? poll.options.length : 0;
264
+ if (optionCount > maxOptions) {
265
+ throw new Error(`[[poll:error.max_options, ${maxOptions}]]`);
266
+ }
267
+ });
268
+ }
269
+
270
+ async function validateMaxOptionsForEdit(updatedPolls, currentPolls) {
271
+ if (!Array.isArray(updatedPolls) || !updatedPolls.length) {
272
+ return;
273
+ }
274
+ const settings = await Config.getSettings();
275
+ const { maxOptions } = settings;
276
+ updatedPolls.forEach((poll) => {
277
+ if (!poll) {
278
+ return;
279
+ }
280
+ const newOptionCount = Array.isArray(poll.options) ? poll.options.length : 0;
281
+ if (newOptionCount <= maxOptions) {
282
+ return;
283
+ }
284
+ const currentPoll = currentPolls.find(p => p && String(p.pollId) === String(poll.pollId));
285
+ const currentOptions = currentPoll ? tryParseOptions(currentPoll.options) : [];
286
+ const currentOptionCount = Array.isArray(currentOptions) ? currentOptions.length : 0;
287
+ if (newOptionCount > currentOptionCount) {
288
+ throw new Error(`[[poll:error.max_options, ${maxOptions}]]`);
289
+ }
290
+ });
291
+ }
292
+
248
293
  Poll.getOption = async function (pollId, option, withVotes = false) {
249
294
  const [optionsJson, votes, voteCount] = await Promise.all([
250
295
  db.getObjectField(`poll:${pollId}`, 'options'),
package/lib/sockets.js CHANGED
@@ -15,7 +15,7 @@ const Sockets = exports;
15
15
 
16
16
  Sockets.get = async function (socket, data) {
17
17
  const settings = await Config.getSettings();
18
- const allowAnon = settings.allowGuestsToViewResults === 'on';
18
+ const allowAnon = !!settings.allowGuestsToViewResults;
19
19
 
20
20
  if (!data) {
21
21
  throw new Error('Invalid request, request data is not defined');
@@ -27,13 +27,13 @@ Sockets.get = async function (socket, data) {
27
27
  return [];
28
28
  }
29
29
  return await Promise.all(data.pollIds.map(async (pollId) => {
30
- const withVotes = allowAnon || socket.uid > 0;
30
+ const withVotes = !!socket.uid;
31
31
  const pollData = await Poll.get(pollId, socket.uid, withVotes);
32
32
  if (!pollData) {
33
33
  return null;
34
34
  }
35
35
 
36
- if (parseInt(pollData.info.allowAnonVoting, 10) === 1) {
36
+ if (withVotes && parseInt(pollData.info.allowAnonVoting, 10) === 1) {
37
37
  await anonymizeVoters(socket.uid, pollId, pollData.options);
38
38
  }
39
39
 
@@ -145,20 +145,18 @@ Sockets.removeVote = async function (socket, data) {
145
145
  };
146
146
 
147
147
  Sockets.getOptionDetails = async function (socket, data) {
148
- if (!data || isNaN(parseInt(data.pollId, 10)) || isNaN(parseInt(data.optionId, 10))) {
148
+ if (!socket.uid || !data || isNaN(parseInt(data.pollId, 10)) || isNaN(parseInt(data.optionId, 10))) {
149
149
  throw new Error('Invalid request');
150
150
  }
151
- const [poll, option, settings] = await Promise.all([
151
+ const [poll, option] = await Promise.all([
152
152
  Poll.getInfo(data.pollId),
153
153
  Poll.getOption(data.pollId, data.optionId, true),
154
- Config.getSettings(),
155
154
  ]);
156
155
  if (!poll) {
157
156
  throw new Error('[[error:invalid-data]]');
158
157
  }
159
- const allowAnon = settings.allowGuestsToViewResults === 'on';
160
158
  const canRead = await privileges.posts.can('topics:read', poll.pid, socket.uid);
161
- if (!canRead || (!allowAnon && socket.uid <= 0)) {
159
+ if (!canRead) {
162
160
  throw new Error('[[error:no-privileges]]');
163
161
  }
164
162
  if (!option || !option.votes || !option.votes.length) {
@@ -170,7 +168,7 @@ Sockets.getOptionDetails = async function (socket, data) {
170
168
  }
171
169
 
172
170
  const userData = await NodeBB.User.getUsersFields(option.votes, [
173
- 'uid', 'username', 'userslug', 'picture',
171
+ 'uid', 'username', 'userslug', 'picture', 'displayname', 'icon:text', 'icon:bgColor',
174
172
  ]);
175
173
  option.votes = userData;
176
174
 
@@ -205,7 +203,7 @@ async function anonymizeVoters(callerUid, pollId, options) {
205
203
  }
206
204
  const uidToIsAnon = _.zipObject(uids, isAnon);
207
205
  options.forEach((opt) => {
208
- opt.votes = opt.votes.map(
206
+ opt.votes = (opt.votes || []).map(
209
207
  uid => (uidToIsAnon[uid] && String(callerUid) !== uid ? 0 : uid)
210
208
  );
211
209
  });
package/library.js CHANGED
@@ -92,7 +92,7 @@ Plugin.renderPollWidget = async function (widget) {
92
92
  const Poll = require('./lib/poll');
93
93
 
94
94
  const settings = await Config.getSettings();
95
- const allowAnon = settings.allowGuestsToViewResults === 'on';
95
+ const allowAnon = !!settings.allowGuestsToViewResults;
96
96
 
97
97
  if (!widget.uid && !allowAnon) {
98
98
  return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodebb-plugin-poll",
3
- "version": "4.0.14",
3
+ "version": "4.0.15",
4
4
  "description": "NodeBB Poll Plugin",
5
5
  "main": "library.js",
6
6
  "scripts": {
@@ -46,7 +46,6 @@
46
46
  const html = await Benchpress.render('poll/manage', { post: postData });
47
47
 
48
48
  if (modal) {
49
- // modal.find('.bootbox-body').html(html[0].outerHTML);
50
49
  modal.find('.bootbox-body').html(html);
51
50
  handleSort({ modal, polls: postData.polls });
52
51
  } else {
@@ -158,7 +157,16 @@
158
157
  Creator.show = function (modalTitle, poll, config) {
159
158
  return new Promise((resolve) => {
160
159
  app.require(['benchpressjs', 'modals']).then(([Benchpress, modals]) => {
161
- Benchpress.render('poll/creator', { poll, config }).then(async function (html) {
160
+ const pollForTemplate = { ...poll };
161
+ const end = parseInt(pollForTemplate.end, 10);
162
+ if (end > 0) {
163
+ const d = new Date(end);
164
+ const pad = n => String(n).padStart(2, '0');
165
+ pollForTemplate.end = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
166
+ } else {
167
+ pollForTemplate.end = '';
168
+ }
169
+ Benchpress.render('poll/creator', { poll: pollForTemplate, config }).then(async function (html) {
162
170
  const modal = await modals.dialog({
163
171
  title: modalTitle,
164
172
  message: html,
@@ -190,14 +198,15 @@
190
198
  },
191
199
  });
192
200
 
201
+ setTimezoneLabel(modal);
193
202
  handleOptionSort({ modal });
194
203
 
195
204
  // Add option adder
196
205
  modal.find('#pollAddOption')
197
206
  .off('click')
198
207
  .on('click', async function () {
199
- const optionEls = modal.find('[component="post/poll/option/item"]');
200
- if (config.maxOptions <= optionEls.length) {
208
+ const optionCount = modal.find('#poll-options-container [component="post/poll/option/item"]').length;
209
+ if (config.maxOptions <= optionCount) {
201
210
  clearErrors();
202
211
  error(`[[poll:error.max_options, ${config.maxOptions}]]`);
203
212
  return false;
@@ -230,6 +239,14 @@
230
239
  });
231
240
  }
232
241
 
242
+ function setTimezoneLabel(modal) {
243
+ const offsetMinutes = -new Date().getTimezoneOffset();
244
+ const sign = offsetMinutes >= 0 ? '+' : '-';
245
+ const hours = String(Math.floor(Math.abs(offsetMinutes) / 60)).padStart(2, '0');
246
+ const minutes = String(Math.abs(offsetMinutes) % 60).padStart(2, '0');
247
+ modal.find('[data-timezone-label]').text(`(UTC${sign}${hours}:${minutes})`);
248
+ }
249
+
233
250
  async function error(message) {
234
251
  const errorBox = $('#pollErrorBox');
235
252
  const translator = await app.require('translator');
@@ -71,16 +71,27 @@ $(document).ready(function () {
71
71
  });
72
72
 
73
73
  function getPollByPost(post) {
74
- if (!post || !post.hasOwnProperty('pollIds') || !post.pollIds) return;
75
- let pollIds;
76
- try {
77
- pollIds = JSON.parse(post.pollIds || '[]');
78
- } catch (err) {
79
- pollIds = [];
74
+ if (!post) return;
75
+ let pollIds = [];
76
+ if (post.pollIds) {
77
+ try {
78
+ pollIds = JSON.parse(post.pollIds);
79
+ } catch (err) {
80
+ pollIds = [];
81
+ }
80
82
  }
83
+ const postEl = $(`[component="post"][data-pid="${post.pid}"]`);
84
+ const container = postEl.find('[component="post/content"]');
85
+ const validPollIdSet = new Set((pollIds || []).map(id => String(id)));
86
+ container.find('[data-poll-id]').each(function () {
87
+ const existingPollId = $(this).attr('data-poll-id');
88
+ if (!validPollIdSet.has(existingPollId)) {
89
+ $(this).remove();
90
+ delete Poll.view.polls[existingPollId];
91
+ }
92
+ });
81
93
  if (pollIds.length) {
82
- const postEl = $(`[component="post"][data-pid="${post.pid}"]`);
83
- getPolls(pollIds, postEl.find('[component="post/content"]'));
94
+ getPolls(pollIds, container);
84
95
  }
85
96
  }
86
97
 
@@ -129,6 +129,9 @@
129
129
  {
130
130
  // Option details
131
131
  register: function (view) {
132
+ if (!app.user.uid) {
133
+ return;
134
+ }
132
135
  const self = this;
133
136
  view.dom.resultsPanel.off('click').on('click', '.poll-result-votecount', function (e) {
134
137
  self.handle(view, e);
@@ -201,7 +204,7 @@
201
204
  };
202
205
 
203
206
  View.prototype.voteUpdateAllowed = function () {
204
- return parseInt(this.pollData.disallowVoteUpdate, 10) !== 1;
207
+ return parseInt(this.pollData.info.disallowVoteUpdate, 10) !== 1;
205
208
  };
206
209
 
207
210
  View.prototype.pollEndedOrDeleted = function () {
@@ -257,20 +260,26 @@
257
260
  this.dom.messages.addClass('hidden');
258
261
  };
259
262
 
260
- View.prototype.showOptionDetails = function (details) {
261
- require(['benchpress', 'modals'], function (benchpress, modals) {
262
- benchpress.render('poll/view/details', details).then(function (html) {
263
- modals.dialog({
264
- title: `[[poll:x-users-voted-for-this-option, ${details.voteCount}]]`,
265
- message: html,
266
- backdrop: true,
267
- buttons: {
268
- close: {
269
- label: 'Close',
270
- },
271
- },
272
- });
273
- });
263
+ View.prototype.showOptionDetails = async function (details) {
264
+ const [modals, Benchpress] = await app.require(['modals', 'benchpressjs']);
265
+ if (details.votes) {
266
+ const translator = await app.require('translator');
267
+ await Promise.all(details.votes.map(async (voter) => {
268
+ if (!parseInt(voter.uid, 10) && voter.displayname) {
269
+ voter.displayname = await translator.translate(voter.displayname);
270
+ }
271
+ }));
272
+ }
273
+ const html = await Benchpress.render('poll/view/details', details);
274
+ await modals.dialog({
275
+ title: `[[poll:x-users-voted-for-this-option, ${details.voteCount}]]`,
276
+ message: html,
277
+ backdrop: true,
278
+ buttons: {
279
+ close: {
280
+ label: '[[global:close]]',
281
+ },
282
+ },
274
283
  });
275
284
  };
276
285
 
@@ -301,7 +310,7 @@
301
310
  }
302
311
  } else {
303
312
  this.showVoteButton();
304
- if (this.pollData?.info?.allowAnonVoting) {
313
+ if (this.pollData.info.allowAnonVoting) {
305
314
  this.showVoteAnonButton();
306
315
  }
307
316
  }
@@ -4,7 +4,7 @@
4
4
  <h4 class="fw-bold tracking-tight mb-0">{title}</h4>
5
5
  </div>
6
6
  <div class="col-12 col-md-4 px-0 px-md-3 text-end">
7
- <button id="save" class="btn btn-primary btn-sm fw-semibold ff-secondary text-center text-nowrap">[[admin/admin:save-changes]]</button>
7
+ <button id="save" class="btn btn-primary btn-sm fw-semibold ff-secondary text-center text-nowrap">{{tx("admin/admin:save-changes")}}</button>
8
8
  </div>
9
9
  </div>
10
10
 
@@ -15,10 +15,10 @@
15
15
  <thead>
16
16
  <tr class="text-sm">
17
17
  <th>ID</th>
18
- <th class="text-nowrap">[[poll:poll_title]]</th>
19
- <th class="text-nowrap">[[poll:created-time]]</th>
20
- <th class="text-nowrap">[[poll:end-time]]</th>
21
- <th class="text-end text-nowrap text-capitalize">[[poll:votes]]</th>
18
+ <th class="text-nowrap">{{tx("poll:poll_title")}}</th>
19
+ <th class="text-nowrap">{{tx("poll:created-time")}}</th>
20
+ <th class="text-nowrap">{{tx("poll:end-time")}}</th>
21
+ <th class="text-end text-nowrap text-capitalize">{{tx("poll:votes")}}</th>
22
22
  </tr>
23
23
  </thead>
24
24
  <tbody class="text-xs text-tabular">
@@ -47,27 +47,26 @@
47
47
  <div id="spy-container" class="col-12 col-lg-3" tabindex="0">
48
48
  <form class="form poll-settings">
49
49
  <div class="form-check mb-3">
50
- <label class="form-check-label" for="allowGuestsToViewResults">[[poll:allow_guests]]</label>
50
+ <label class="form-check-label" for="allowGuestsToViewResults">{{tx("poll:allow_guests")}}</label>
51
51
  <input class="form-check-input" type="checkbox" id="allowGuestsToViewResults" name="allowGuestsToViewResults">
52
52
  </div>
53
53
 
54
54
  <div class="mb-3">
55
- <label class="form-label" for="maxOptions">[[poll:max_options]]</label>
55
+ <label class="form-label" for="maxOptions">{{tx("poll:max_options")}}</label>
56
56
  <input type="number" class="form-control" id="maxOptions" placeholder="10" min="1" max="100" name="maxOptions">
57
57
  </div>
58
58
 
59
59
  <div class="mb-3">
60
- <label class="form-label" for="defaultTitle">[[poll:default_title]]</label>
60
+ <label class="form-label" for="defaultTitle">{{tx("poll:default_title")}}</label>
61
61
  <input type="text" class="form-control" id="defaultTitle" placeholder="Poll" name="defaultTitle">
62
62
  </div>
63
63
  <div>
64
- <label class="form-label" for="maximumVotesPerUser">[[poll:max_votes]]</label>
64
+ <label class="form-label" for="maximumVotesPerUser">{{tx("poll:max_votes")}}</label>
65
65
  <input type="number" class="form-control" id="maximumVotesPerUser" placeholder="1" min="1" max="100" name="maximumVotesPerUser">
66
- <p class="form-text">[[poll:info_choices]]</p>
66
+ <p class="form-text">{{tx("poll:info_choices")}}</p>
67
67
  </div>
68
68
  </form>
69
69
  </div>
70
70
  </div>
71
71
  </div>
72
72
 
73
-
@@ -4,12 +4,12 @@
4
4
  <input type="hidden" name="pollId" value="{poll.pollId}">
5
5
 
6
6
  <div class="mb-3">
7
- <label class="form-label" for="pollInputTitle">[[poll:poll_title]]</label>
8
- <input type="text" name="title" id="pollInputTitle" value="{poll.title}" placeholder="[[poll:poll_title_placeholder]]" class="form-control">
7
+ <label class="form-label" for="pollInputTitle">{{tx("poll:poll_title")}}</label>
8
+ <input type="text" name="title" id="pollInputTitle" value="{poll.title}" placeholder="{{tx("poll:poll_title_placeholder")}}" class="form-control">
9
9
  </div>
10
10
 
11
11
  <div class="mb-3 d-flex flex-column gap-1">
12
- <label class="form-label" for="pollInputOptions">[[poll:options_title]]</label>
12
+ <label class="form-label" for="pollInputOptions">{{tx("poll:options_title")}}</label>
13
13
  <div id="poll-options-container" class="d-flex flex-column gap-1">
14
14
  {{{ if poll.options.length }}}
15
15
  {{{ each poll.options }}}
@@ -17,32 +17,33 @@
17
17
  {{{ end }}}
18
18
  {{{ end }}}
19
19
  </div>
20
- <button type="button" id="pollAddOption" class="btn btn-primary btn-sm btn-block mt-2">[[poll:options_add]]</button>
20
+ <button type="button" id="pollAddOption" class="btn btn-primary btn-sm btn-block mt-2">{{tx("poll:options_add")}}</button>
21
21
  </div>
22
22
 
23
23
  <hr>
24
24
 
25
25
  <div class="mb-3">
26
- <label class="form-label" for="pollInputAmount">[[poll:max_votes]]</label>
27
- <input type="number" name="maximumVotesPerUser" id="pollInputAmount" value="{poll.maximumVotesPerUser}" min="1" max="10" step="1" placeholder="[[poll:max_votes_placeholder]]" class="form-control">
28
- <p class="form-text">[[poll:info_choices]]</p>
26
+ <label class="form-label" for="pollInputAmount">{{tx("poll:max_votes")}}</label>
27
+ <input type="number" name="maximumVotesPerUser" id="pollInputAmount" value="{poll.maximumVotesPerUser}" min="1" max="10" step="1" placeholder="{{tx("poll:max_votes_placeholder")}}" class="form-control">
28
+ <p class="form-text">{{tx("poll:info_choices")}}</p>
29
29
  </div>
30
30
 
31
31
  <div class="form-check mb-3">
32
- <label class="form-check-label" for="pollDisallowVoteUpdate">[[poll:disallow_vote_update]]</label>
32
+ <label class="form-check-label" for="pollDisallowVoteUpdate">{{tx("poll:disallow_vote_update")}}</label>
33
33
  <input class="form-check-input" type="checkbox" name="disallowVoteUpdate" id="pollDisallowVoteUpdate" {{{if poll.disallowVoteUpdate}}}checked{{{end}}}>
34
34
  </div>
35
35
 
36
36
  <div class="form-check mb-3">
37
- <label class="form-check-label" for="allowAnonVoting">[[poll:allow_anon_voting]]</label>
37
+ <label class="form-check-label" for="allowAnonVoting">{{tx("poll:allow_anon_voting")}}</label>
38
38
  <input class="form-check-input" type="checkbox" name="allowAnonVoting" id="allowAnonVoting" {{{if poll.allowAnonVoting}}}checked{{{end}}}>
39
39
  </div>
40
40
 
41
41
  <div class="mb-3">
42
- <label class="form-label" for="pollInputEnd">[[poll:auto_end_title]]</label>
42
+ <label class="form-label" for="pollInputEnd">{{tx("poll:auto_end_title")}}</label>
43
43
  <div class="input-group date">
44
- <input id="pollInputEnd" placeholder="[[poll:auto_end_placeholder]]" name="end" class="form-control" value="{poll.end}" type="datetime-local">
44
+ <input id="pollInputEnd" placeholder="{{tx("poll:auto_end_placeholder")}}" name="end" class="form-control" value="{poll.end}" type="datetime-local">
45
+ <span class="input-group-text poll-timezone-label" data-timezone-label></span>
45
46
  </div>
46
- <p class="form-text">[[poll:auto_end_help]]</p>
47
+ <p class="form-text">{{tx("poll:auto_end_help")}}</p>
47
48
  </div>
48
49
  </form>
@@ -1,6 +1,6 @@
1
1
  <div class="topic-poll-modal">
2
2
  {{{ if !post.polls.length }}}
3
- <div class="alert alert-info">[[poll:no-polls]]</div>
3
+ <div class="alert alert-info">{{tx("poll:no-polls")}}</div>
4
4
  {{{ end }}}
5
5
  {{{ each post.polls }}}
6
6
  <div component="post/poll/item" data-poll-id="{./pollId}" class="bg-body d-flex align-items-center justify-content-between mb-3 p-1 border rounded-1">
@@ -1,3 +1,3 @@
1
- <h5>{title}</h5>
2
- <p>{content}</p>
1
+ <h5>{{tx(title)}}</h5>
2
+ <p>{{tx(content)}}</p>
3
3
  <hr>
@@ -1,10 +1,16 @@
1
- <!-- BEGIN poll.options -->
1
+ {{{ each poll.options }}}
2
2
  <div class="poll-result" data-poll-option-id="{poll.options.id}">
3
3
  <div class="d-flex justify-content-between mb-1">
4
4
  <strong>{poll.options.title}</strong>
5
+ {{{ if config.loggedIn }}}
5
6
  <a class="poll-result-votecount small" href="#">
6
- <span>[[poll:x-votes, {poll.options.voteCount}]]</span>
7
+ <span>{{tx("poll:x-votes", poll.options.voteCount)}}</span>
7
8
  </a>
9
+ {{{ else }}}
10
+ <span class="poll-result-votecount small">
11
+ <span>{{tx("poll:x-votes", poll.options.voteCount)}}</span>
12
+ </span>
13
+ {{{ end }}}
8
14
  </div>
9
15
  <div class="progress">
10
16
  <div class="progress-bar poll-result-progressbar" role="progressbar" aria-valuenow="{poll.options.percentage}" aria-valuemin="0" aria-valuemax="100" style="width: {poll.options.percentage}%;">
@@ -12,4 +18,4 @@
12
18
  </div>
13
19
  </div>
14
20
  </div>
15
- <!-- END poll.options -->
21
+ {{{ end }}}