##// END OF EJS Templates
Fixed sticker autocompletions. Localized 'too many files' message and added max file count there
neko259 -
r1766:8d73e763 default
parent child Browse files
Show More
@@ -1,483 +1,486
1 1 import hashlib
2 2 import logging
3 3 import re
4 4 import time
5 5
6 6 import pytz
7 7 from django import forms
8 8 from django.core.files.uploadedfile import SimpleUploadedFile, UploadedFile
9 9 from django.forms.utils import ErrorList
10 10 from django.utils.translation import ugettext_lazy as _, ungettext_lazy
11 11
12 12 import boards.settings as board_settings
13 13 import neboard
14 14 from boards.abstracts.attachment_alias import get_image_by_alias
15 15 from boards.abstracts.settingsmanager import get_settings_manager
16 16 from boards.forms.fields import UrlFileField
17 17 from boards.mdx_neboard import formatters
18 18 from boards.models import Tag
19 19 from boards.models.attachment.downloaders import download, REGEX_MAGNET
20 20 from boards.models.post import TITLE_MAX_LENGTH
21 21 from boards.utils import validate_file_size, get_file_mimetype, \
22 22 FILE_EXTENSION_DELIMITER
23 23 from neboard import settings
24 24
25 25 SECTION_FORMS = 'Forms'
26 26
27 27 POW_HASH_LENGTH = 16
28 28 POW_LIFE_MINUTES = 5
29 29
30 30 REGEX_TAGS = re.compile(r'^[\w\s\d]+$', re.UNICODE)
31 31 REGEX_USERNAMES = re.compile(r'^[\w\s\d,]+$', re.UNICODE)
32 32 REGEX_URL = re.compile(r'^(http|https|ftp):\/\/', re.UNICODE)
33 33
34 34 VETERAN_POSTING_DELAY = 5
35 35
36 36 ATTRIBUTE_PLACEHOLDER = 'placeholder'
37 37 ATTRIBUTE_ROWS = 'rows'
38 38
39 39 LAST_POST_TIME = 'last_post_time'
40 40 LAST_LOGIN_TIME = 'last_login_time'
41 41 TEXT_PLACEHOLDER = _('Type message here. Use formatting panel for more advanced usage.')
42 42 TAGS_PLACEHOLDER = _('music images i_dont_like_tags')
43 43
44 44 LABEL_TITLE = _('Title')
45 45 LABEL_TEXT = _('Text')
46 46 LABEL_TAG = _('Tag')
47 47 LABEL_SEARCH = _('Search')
48 48 LABEL_FILE = _('File')
49 49
50 50 ERROR_SPEED = 'Please wait %(delay)d second before sending message'
51 51 ERROR_SPEED_PLURAL = 'Please wait %(delay)d seconds before sending message'
52 ERROR_MANY_FILES = _('Too many files.')
52 ERROR_MANY_FILES = 'You can post no more than %(files)d file.'
53 ERROR_MANY_FILES_PLURAL = 'You can post no more than %(files)d files.'
53 54
54 55 TAG_MAX_LENGTH = 20
55 56
56 57 TEXTAREA_ROWS = 4
57 58
58 59 TRIPCODE_DELIM = '#'
59 60
60 61 # TODO Maybe this may be converted into the database table?
61 62 MIMETYPE_EXTENSIONS = {
62 63 'image/jpeg': 'jpeg',
63 64 'image/png': 'png',
64 65 'image/gif': 'gif',
65 66 'video/webm': 'webm',
66 67 'application/pdf': 'pdf',
67 68 'x-diff': 'diff',
68 69 'image/svg+xml': 'svg',
69 70 'application/x-shockwave-flash': 'swf',
70 71 'image/x-ms-bmp': 'bmp',
71 72 'image/bmp': 'bmp',
72 73 }
73 74
74 75
75 76 logger = logging.getLogger('boards.forms')
76 77
77 78
78 79 def get_timezones():
79 80 timezones = []
80 81 for tz in pytz.common_timezones:
81 82 timezones.append((tz, tz),)
82 83 return timezones
83 84
84 85
85 86 class FormatPanel(forms.Textarea):
86 87 """
87 88 Panel for text formatting. Consists of buttons to add different tags to the
88 89 form text area.
89 90 """
90 91
91 92 def render(self, name, value, attrs=None):
92 93 output = '<div id="mark-panel">'
93 94 for formatter in formatters:
94 95 output += '<span class="mark_btn"' + \
95 96 ' onClick="addMarkToMsg(\'' + formatter.format_left + \
96 97 '\', \'' + formatter.format_right + '\')">' + \
97 98 formatter.preview_left + formatter.name + \
98 99 formatter.preview_right + '</span>'
99 100
100 101 output += '</div>'
101 102 output += super(FormatPanel, self).render(name, value, attrs=attrs)
102 103
103 104 return output
104 105
105 106
106 107 class PlainErrorList(ErrorList):
107 108 def __unicode__(self):
108 109 return self.as_text()
109 110
110 111 def as_text(self):
111 112 return ''.join(['(!) %s ' % e for e in self])
112 113
113 114
114 115 class NeboardForm(forms.Form):
115 116 """
116 117 Form with neboard-specific formatting.
117 118 """
118 119 required_css_class = 'required-field'
119 120
120 121 def as_div(self):
121 122 """
122 123 Returns this form rendered as HTML <as_div>s.
123 124 """
124 125
125 126 return self._html_output(
126 127 # TODO Do not show hidden rows in the list here
127 128 normal_row='<div class="form-row">'
128 129 '<div class="form-label">'
129 130 '%(label)s'
130 131 '</div>'
131 132 '<div class="form-input">'
132 133 '%(field)s'
133 134 '</div>'
134 135 '</div>'
135 136 '<div class="form-row">'
136 137 '%(help_text)s'
137 138 '</div>',
138 139 error_row='<div class="form-row">'
139 140 '<div class="form-label"></div>'
140 141 '<div class="form-errors">%s</div>'
141 142 '</div>',
142 143 row_ender='</div>',
143 144 help_text_html='%s',
144 145 errors_on_separate_row=True)
145 146
146 147 def as_json_errors(self):
147 148 errors = []
148 149
149 150 for name, field in list(self.fields.items()):
150 151 if self[name].errors:
151 152 errors.append({
152 153 'field': name,
153 154 'errors': self[name].errors.as_text(),
154 155 })
155 156
156 157 return errors
157 158
158 159
159 160 class PostForm(NeboardForm):
160 161
161 162 title = forms.CharField(max_length=TITLE_MAX_LENGTH, required=False,
162 163 label=LABEL_TITLE,
163 164 widget=forms.TextInput(
164 165 attrs={ATTRIBUTE_PLACEHOLDER: 'title#tripcode'}))
165 166 text = forms.CharField(
166 167 widget=FormatPanel(attrs={
167 168 ATTRIBUTE_PLACEHOLDER: TEXT_PLACEHOLDER,
168 169 ATTRIBUTE_ROWS: TEXTAREA_ROWS,
169 170 }),
170 171 required=False, label=LABEL_TEXT)
171 172 file = UrlFileField(required=False, label=LABEL_FILE)
172 173
173 174 # This field is for spam prevention only
174 175 email = forms.CharField(max_length=100, required=False, label=_('e-mail'),
175 176 widget=forms.TextInput(attrs={
176 177 'class': 'form-email'}))
177 178 subscribe = forms.BooleanField(required=False, label=_('Subscribe to thread'))
178 179
179 180 guess = forms.CharField(widget=forms.HiddenInput(), required=False)
180 181 timestamp = forms.CharField(widget=forms.HiddenInput(), required=False)
181 182 iteration = forms.CharField(widget=forms.HiddenInput(), required=False)
182 183
183 184 session = None
184 185 need_to_ban = False
185 186 image = None
186 187
187 188 def clean_title(self):
188 189 title = self.cleaned_data['title']
189 190 if title:
190 191 if len(title) > TITLE_MAX_LENGTH:
191 192 raise forms.ValidationError(_('Title must have less than %s '
192 193 'characters') %
193 194 str(TITLE_MAX_LENGTH))
194 195 return title
195 196
196 197 def clean_text(self):
197 198 text = self.cleaned_data['text'].strip()
198 199 if text:
199 200 max_length = board_settings.get_int(SECTION_FORMS, 'MaxTextLength')
200 201 if len(text) > max_length:
201 202 raise forms.ValidationError(_('Text must have less than %s '
202 203 'characters') % str(max_length))
203 204 return text
204 205
205 206 def clean_file(self):
206 207 return self._clean_files(self.cleaned_data['file'])
207 208
208 209 def clean(self):
209 210 cleaned_data = super(PostForm, self).clean()
210 211
211 212 if cleaned_data['email']:
212 213 if board_settings.get_bool(SECTION_FORMS, 'Autoban'):
213 214 self.need_to_ban = True
214 215 raise forms.ValidationError('A human cannot enter a hidden field')
215 216
216 217 if not self.errors:
217 218 self._clean_text_file()
218 219
219 220 limit_speed = board_settings.get_bool(SECTION_FORMS, 'LimitPostingSpeed')
220 221 limit_first = board_settings.get_bool(SECTION_FORMS, 'LimitFirstPosting')
221 222
222 223 settings_manager = get_settings_manager(self)
223 224 if not self.errors and limit_speed or (limit_first and not settings_manager.get_setting('confirmed_user')):
224 225 pow_difficulty = board_settings.get_int(SECTION_FORMS, 'PowDifficulty')
225 226 if pow_difficulty > 0:
226 227 # PoW-based
227 228 if cleaned_data['timestamp'] \
228 229 and cleaned_data['iteration'] and cleaned_data['guess'] \
229 230 and not settings_manager.get_setting('confirmed_user'):
230 231 self._validate_hash(cleaned_data['timestamp'], cleaned_data['iteration'], cleaned_data['guess'], cleaned_data['text'])
231 232 else:
232 233 # Time-based
233 234 self._validate_posting_speed()
234 235 settings_manager.set_setting('confirmed_user', True)
235 236
236 237 return cleaned_data
237 238
238 239 def get_files(self):
239 240 """
240 241 Gets file from form or URL.
241 242 """
242 243
243 244 files = []
244 245 for file in self.cleaned_data['file']:
245 246 if isinstance(file, UploadedFile):
246 247 files.append(file)
247 248
248 249 return files
249 250
250 251 def get_file_urls(self):
251 252 files = []
252 253 for file in self.cleaned_data['file']:
253 254 if type(file) == str:
254 255 files.append(file)
255 256
256 257 return files
257 258
258 259 def get_tripcode(self):
259 260 title = self.cleaned_data['title']
260 261 if title is not None and TRIPCODE_DELIM in title:
261 262 code = title.split(TRIPCODE_DELIM, maxsplit=1)[1] + neboard.settings.SECRET_KEY
262 263 tripcode = hashlib.md5(code.encode()).hexdigest()
263 264 else:
264 265 tripcode = ''
265 266 return tripcode
266 267
267 268 def get_title(self):
268 269 title = self.cleaned_data['title']
269 270 if title is not None and TRIPCODE_DELIM in title:
270 271 return title.split(TRIPCODE_DELIM, maxsplit=1)[0]
271 272 else:
272 273 return title
273 274
274 275 def get_images(self):
275 276 if self.image:
276 277 return [self.image]
277 278 else:
278 279 return []
279 280
280 281 def is_subscribe(self):
281 282 return self.cleaned_data['subscribe']
282 283
283 284 def _update_file_extension(self, file):
284 285 if file:
285 286 mimetype = get_file_mimetype(file)
286 287 extension = MIMETYPE_EXTENSIONS.get(mimetype)
287 288 if extension:
288 289 filename = file.name.split(FILE_EXTENSION_DELIMITER, 1)[0]
289 290 new_filename = filename + FILE_EXTENSION_DELIMITER + extension
290 291
291 292 file.name = new_filename
292 293 else:
293 294 logger.info('Unrecognized file mimetype: {}'.format(mimetype))
294 295
295 296 def _clean_files(self, inputs):
296 297 files = []
297 298
298 299 max_file_count = board_settings.get_int(SECTION_FORMS, 'MaxFileCount')
299 300 if len(inputs) > max_file_count:
300 raise forms.ValidationError(ERROR_MANY_FILES)
301 raise forms.ValidationError(
302 ungettext_lazy(ERROR_MANY_FILES, ERROR_MANY_FILES,
303 max_file_count) % {'files': max_file_count})
301 304 for file_input in inputs:
302 305 if isinstance(file_input, UploadedFile):
303 306 files.append(self._clean_file_file(file_input))
304 307 else:
305 308 files.append(self._clean_file_url(file_input))
306 309
307 310 return files
308 311
309 312 def _clean_file_file(self, file):
310 313 validate_file_size(file.size)
311 314 self._update_file_extension(file)
312 315
313 316 return file
314 317
315 318 def _clean_file_url(self, url):
316 319 file = None
317 320
318 321 if url:
319 322 try:
320 323 file = get_image_by_alias(url, self.session)
321 324 self.image = file
322 325
323 326 if file is not None:
324 327 return
325 328
326 329 if file is None:
327 330 file = self._get_file_from_url(url)
328 331 if not file:
329 332 raise forms.ValidationError(_('Invalid URL'))
330 333 else:
331 334 validate_file_size(file.size)
332 335 self._update_file_extension(file)
333 336 except forms.ValidationError as e:
334 337 # Assume we will get the plain URL instead of a file and save it
335 338 if REGEX_URL.match(url) or REGEX_MAGNET.match(url):
336 339 logger.info('Error in forms: {}'.format(e))
337 340 return url
338 341 else:
339 342 raise e
340 343
341 344 return file
342 345
343 346 def _clean_text_file(self):
344 347 text = self.cleaned_data.get('text')
345 348 file = self.get_files()
346 349 file_url = self.get_file_urls()
347 350 images = self.get_images()
348 351
349 352 if (not text) and (not file) and (not file_url) and len(images) == 0:
350 353 error_message = _('Either text or file must be entered.')
351 354 self._errors['text'] = self.error_class([error_message])
352 355
353 356 def _validate_posting_speed(self):
354 357 can_post = True
355 358
356 359 posting_delay = board_settings.get_int(SECTION_FORMS, 'PostingDelay')
357 360
358 361 if board_settings.get_bool(SECTION_FORMS, 'LimitPostingSpeed'):
359 362 now = time.time()
360 363
361 364 current_delay = 0
362 365
363 366 if LAST_POST_TIME not in self.session:
364 367 self.session[LAST_POST_TIME] = now
365 368
366 369 need_delay = True
367 370 else:
368 371 last_post_time = self.session.get(LAST_POST_TIME)
369 372 current_delay = int(now - last_post_time)
370 373
371 374 need_delay = current_delay < posting_delay
372 375
373 376 if need_delay:
374 377 delay = posting_delay - current_delay
375 378 error_message = ungettext_lazy(ERROR_SPEED, ERROR_SPEED_PLURAL,
376 379 delay) % {'delay': delay}
377 380 self._errors['text'] = self.error_class([error_message])
378 381
379 382 can_post = False
380 383
381 384 if can_post:
382 385 self.session[LAST_POST_TIME] = now
383 386
384 387 def _get_file_from_url(self, url: str) -> SimpleUploadedFile:
385 388 """
386 389 Gets an file file from URL.
387 390 """
388 391
389 392 try:
390 393 return download(url)
391 394 except forms.ValidationError as e:
392 395 raise e
393 396 except Exception as e:
394 397 raise forms.ValidationError(e)
395 398
396 399 def _validate_hash(self, timestamp: str, iteration: str, guess: str, message: str):
397 400 payload = timestamp + message.replace('\r\n', '\n')
398 401 difficulty = board_settings.get_int(SECTION_FORMS, 'PowDifficulty')
399 402 target = str(int(2 ** (POW_HASH_LENGTH * 3) / difficulty))
400 403 if len(target) < POW_HASH_LENGTH:
401 404 target = '0' * (POW_HASH_LENGTH - len(target)) + target
402 405
403 406 computed_guess = hashlib.sha256((payload + iteration).encode())\
404 407 .hexdigest()[0:POW_HASH_LENGTH]
405 408 if guess != computed_guess or guess > target:
406 409 self._errors['text'] = self.error_class(
407 410 [_('Invalid PoW.')])
408 411
409 412
410 413 class ThreadForm(PostForm):
411 414
412 415 tags = forms.CharField(
413 416 widget=forms.TextInput(attrs={ATTRIBUTE_PLACEHOLDER: TAGS_PLACEHOLDER}),
414 417 max_length=100, label=_('Tags'), required=True)
415 418 monochrome = forms.BooleanField(label=_('Monochrome'), required=False)
416 419
417 420 def clean_tags(self):
418 421 tags = self.cleaned_data['tags'].strip()
419 422
420 423 if not tags or not REGEX_TAGS.match(tags):
421 424 raise forms.ValidationError(
422 425 _('Inappropriate characters in tags.'))
423 426
424 427 default_tag_name = board_settings.get(SECTION_FORMS, 'DefaultTag')\
425 428 .strip().lower()
426 429
427 430 required_tag_exists = False
428 431 tag_set = set()
429 432 for tag_string in tags.split():
430 433 if tag_string.strip().lower() == default_tag_name:
431 434 required_tag_exists = True
432 435 tag, created = Tag.objects.get_or_create(
433 436 name=tag_string.strip().lower(), required=True)
434 437 else:
435 438 tag, created = Tag.objects.get_or_create(
436 439 name=tag_string.strip().lower())
437 440 tag_set.add(tag)
438 441
439 442 # If this is a new tag, don't check for its parents because nobody
440 443 # added them yet
441 444 if not created:
442 445 tag_set |= set(tag.get_all_parents())
443 446
444 447 for tag in tag_set:
445 448 if tag.required:
446 449 required_tag_exists = True
447 450 break
448 451
449 452 # Use default tag if no section exists
450 453 if not required_tag_exists:
451 454 default_tag, created = Tag.objects.get_or_create(
452 455 name=default_tag_name, required=True)
453 456 tag_set.add(default_tag)
454 457
455 458 return tag_set
456 459
457 460 def clean(self):
458 461 cleaned_data = super(ThreadForm, self).clean()
459 462
460 463 return cleaned_data
461 464
462 465 def is_monochrome(self):
463 466 return self.cleaned_data['monochrome']
464 467
465 468
466 469 class SettingsForm(NeboardForm):
467 470
468 471 theme = forms.ChoiceField(choices=settings.THEMES, label=_('Theme'))
469 472 image_viewer = forms.ChoiceField(choices=settings.IMAGE_VIEWERS, label=_('Image view mode'))
470 473 username = forms.CharField(label=_('User name'), required=False)
471 474 timezone = forms.ChoiceField(choices=get_timezones(), label=_('Time zone'))
472 475
473 476 def clean_username(self):
474 477 username = self.cleaned_data['username']
475 478
476 479 if username and not REGEX_USERNAMES.match(username):
477 480 raise forms.ValidationError(_('Inappropriate characters.'))
478 481
479 482 return username
480 483
481 484
482 485 class SearchForm(NeboardForm):
483 486 query = forms.CharField(max_length=500, label=LABEL_SEARCH, required=False)
1 NO CONTENT: modified file, binary diff hidden
@@ -1,569 +1,575
1 1 # SOME DESCRIPTIVE TITLE.
2 2 # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3 3 # This file is distributed under the same license as the PACKAGE package.
4 4 # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
5 5 #
6 6 msgid ""
7 7 msgstr ""
8 8 "Project-Id-Version: PACKAGE VERSION\n"
9 9 "Report-Msgid-Bugs-To: \n"
10 10 "POT-Creation-Date: 2015-10-09 23:21+0300\n"
11 11 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
12 12 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
13 13 "Language-Team: LANGUAGE <LL@li.org>\n"
14 14 "Language: ru\n"
15 15 "MIME-Version: 1.0\n"
16 16 "Content-Type: text/plain; charset=UTF-8\n"
17 17 "Content-Transfer-Encoding: 8bit\n"
18 18 "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
19 19 "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
20 20
21 21 #: admin.py:22
22 22 msgid "{} posters were banned"
23 23 msgstr ""
24 24
25 25 #: authors.py:9
26 26 msgid "author"
27 27 msgstr "Π°Π²Ρ‚ΠΎΡ€"
28 28
29 29 #: authors.py:10
30 30 msgid "developer"
31 31 msgstr "Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊ"
32 32
33 33 #: authors.py:11
34 34 msgid "javascript developer"
35 35 msgstr "Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊ javascript"
36 36
37 37 #: authors.py:12
38 38 msgid "designer"
39 39 msgstr "Π΄ΠΈΠ·Π°ΠΉΠ½Π΅Ρ€"
40 40
41 41 #: forms.py:30
42 42 msgid "Type message here. Use formatting panel for more advanced usage."
43 43 msgstr ""
44 44 "Π’Π²ΠΎΠ΄ΠΈΡ‚Π΅ сообщСниС сюда. Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠΉΡ‚Π΅ панСль для Π±ΠΎΠ»Π΅Π΅ слоТного форматирования."
45 45
46 46 #: forms.py:31
47 47 msgid "music images i_dont_like_tags"
48 48 msgstr "ΠΌΡƒΠ·Ρ‹ΠΊΠ° ΠΊΠ°Ρ€Ρ‚ΠΈΠ½ΠΊΠΈ Ρ‚Π΅Π³ΠΈ_Π½Π΅_Π½ΡƒΠΆΠ½Ρ‹"
49 49
50 50 #: forms.py:33
51 51 msgid "Title"
52 52 msgstr "Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ"
53 53
54 54 #: forms.py:34
55 55 msgid "Text"
56 56 msgstr "ВСкст"
57 57
58 58 #: forms.py:35
59 59 msgid "Tag"
60 60 msgstr "ΠœΠ΅Ρ‚ΠΊΠ°"
61 61
62 62 #: forms.py:36 templates/boards/base.html:40 templates/search/search.html:7
63 63 msgid "Search"
64 64 msgstr "Поиск"
65 65
66 66 #: forms.py:48
67 67 msgid "File 1"
68 68 msgstr "Π€Π°ΠΉΠ» 1"
69 69
70 70 #: forms.py:48
71 71 msgid "File 2"
72 72 msgstr "Π€Π°ΠΉΠ» 2"
73 73
74 74 #: forms.py:142
75 75 msgid "File URL"
76 76 msgstr "URL Ρ„Π°ΠΉΠ»Π°"
77 77
78 78 #: forms.py:148
79 79 msgid "e-mail"
80 80 msgstr ""
81 81
82 82 #: forms.py:151
83 83 msgid "Additional threads"
84 84 msgstr "Π”ΠΎΠΏΠΎΠ»Π½ΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹Π΅ Ρ‚Π΅ΠΌΡ‹"
85 85
86 86 #: forms.py:162
87 87 #, python-format
88 88 msgid "Title must have less than %s characters"
89 89 msgstr "Π—Π°Π³ΠΎΠ»ΠΎΠ²ΠΎΠΊ Π΄ΠΎΠ»ΠΆΠ΅Π½ ΠΈΠΌΠ΅Ρ‚ΡŒ мСньшС %s символов"
90 90
91 91 #: forms.py:172
92 92 #, python-format
93 93 msgid "Text must have less than %s characters"
94 94 msgstr "ВСкст Π΄ΠΎΠ»ΠΆΠ΅Π½ Π±Ρ‹Ρ‚ΡŒ ΠΊΠΎΡ€ΠΎΡ‡Π΅ %s символов"
95 95
96 96 #: forms.py:192
97 97 msgid "Invalid URL"
98 98 msgstr "НСвСрный URL"
99 99
100 100 #: forms.py:213
101 101 msgid "Invalid additional thread list"
102 102 msgstr "НСвСрный список Π΄ΠΎΠΏΠΎΠ»Π½ΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹Ρ… Ρ‚Π΅ΠΌ"
103 103
104 104 #: forms.py:258
105 105 msgid "Either text or file must be entered."
106 106 msgstr "ВСкст ΠΈΠ»ΠΈ Ρ„Π°ΠΉΠ» Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ Π²Π²Π΅Π΄Π΅Π½Ρ‹."
107 107
108 108 #: forms.py:317 templates/boards/all_threads.html:153
109 109 #: templates/boards/rss/post.html:10 templates/boards/tags.html:6
110 110 msgid "Tags"
111 111 msgstr "ΠœΠ΅Ρ‚ΠΊΠΈ"
112 112
113 113 #: forms.py:324
114 114 msgid "Inappropriate characters in tags."
115 115 msgstr "НСдопустимыС символы Π² ΠΌΠ΅Ρ‚ΠΊΠ°Ρ…."
116 116
117 117 #: forms.py:344
118 118 msgid "Need at least one section."
119 119 msgstr "НуТСн хотя Π±Ρ‹ ΠΎΠ΄ΠΈΠ½ Ρ€Π°Π·Π΄Π΅Π»."
120 120
121 121 #: forms.py:356
122 122 msgid "Theme"
123 123 msgstr "Π’Π΅ΠΌΠ°"
124 124
125 125 #: forms.py:357
126 126 msgid "Image view mode"
127 127 msgstr "Π Π΅ΠΆΠΈΠΌ просмотра ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ"
128 128
129 129 #: forms.py:358
130 130 msgid "User name"
131 131 msgstr "Имя ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ"
132 132
133 133 #: forms.py:359
134 134 msgid "Time zone"
135 135 msgstr "Часовой пояс"
136 136
137 137 #: forms.py:365
138 138 msgid "Inappropriate characters."
139 139 msgstr "НСдопустимыС символы."
140 140
141 141 #: templates/boards/404.html:6
142 142 msgid "Not found"
143 143 msgstr "НС найдСно"
144 144
145 145 #: templates/boards/404.html:12
146 146 msgid "This page does not exist"
147 147 msgstr "Π­Ρ‚ΠΎΠΉ страницы Π½Π΅ сущСствуСт"
148 148
149 149 #: templates/boards/all_threads.html:35
150 150 msgid "Details"
151 151 msgstr "ΠŸΠΎΠ΄Ρ€ΠΎΠ±Π½ΠΎΡΡ‚ΠΈ"
152 152
153 153 #: templates/boards/all_threads.html:69
154 154 msgid "Edit tag"
155 155 msgstr "Π˜Π·ΠΌΠ΅Π½ΠΈΡ‚ΡŒ ΠΌΠ΅Ρ‚ΠΊΡƒ"
156 156
157 157 #: templates/boards/all_threads.html:76
158 158 #, python-format
159 159 msgid "%(count)s active thread"
160 160 msgid_plural "%(count)s active threads"
161 161 msgstr[0] "%(count)s активная Ρ‚Π΅ΠΌΠ°"
162 162 msgstr[1] "%(count)s Π°ΠΊΡ‚ΠΈΠ²Π½Ρ‹Π΅ Ρ‚Π΅ΠΌΡ‹"
163 163 msgstr[2] "%(count)s Π°ΠΊΡ‚ΠΈΠ²Π½Ρ‹Ρ… Ρ‚Π΅ΠΌ"
164 164
165 165 #: templates/boards/all_threads.html:76
166 166 #, python-format
167 167 msgid "%(count)s thread in bumplimit"
168 168 msgid_plural "%(count)s threads in bumplimit"
169 169 msgstr[0] "%(count)s Ρ‚Π΅ΠΌΠ° Π² Π±Π°ΠΌΠΏΠ»ΠΈΠΌΠΈΡ‚Π΅"
170 170 msgstr[1] "%(count)s Ρ‚Π΅ΠΌΡ‹ Π² Π±Π°ΠΌΠΏΠ»ΠΈΠΌΠΈΡ‚Π΅"
171 171 msgstr[2] "%(count)s Ρ‚Π΅ΠΌ Π² Π±Π°ΠΌΠΏΠ»ΠΈΠΌΠΈΡ‚Π΅"
172 172
173 173 #: templates/boards/all_threads.html:77
174 174 #, python-format
175 175 msgid "%(count)s archived thread"
176 176 msgid_plural "%(count)s archived thread"
177 177 msgstr[0] "%(count)s архивная Ρ‚Π΅ΠΌΠ°"
178 178 msgstr[1] "%(count)s Π°Ρ€Ρ…ΠΈΠ²Π½Ρ‹Π΅ Ρ‚Π΅ΠΌΡ‹"
179 179 msgstr[2] "%(count)s Π°Ρ€Ρ…ΠΈΠ²Π½Ρ‹Ρ… Ρ‚Π΅ΠΌ"
180 180
181 181 #: templates/boards/all_threads.html:78 templates/boards/post.html:102
182 182 #, python-format
183 183 #| msgid "%(count)s message"
184 184 #| msgid_plural "%(count)s messages"
185 185 msgid "%(count)s message"
186 186 msgid_plural "%(count)s messages"
187 187 msgstr[0] "%(count)s сообщСниС"
188 188 msgstr[1] "%(count)s сообщСния"
189 189 msgstr[2] "%(count)s сообщСний"
190 190
191 191 #: templates/boards/all_threads.html:95 templates/boards/feed.html:30
192 192 #: templates/boards/notifications.html:17 templates/search/search.html:26
193 193 msgid "Previous page"
194 194 msgstr "ΠŸΡ€Π΅Π΄Ρ‹Π΄ΡƒΡ‰Π°Ρ страница"
195 195
196 196 #: templates/boards/all_threads.html:109
197 197 #, python-format
198 198 msgid "Skipped %(count)s reply. Open thread to see all replies."
199 199 msgid_plural "Skipped %(count)s replies. Open thread to see all replies."
200 200 msgstr[0] "ΠŸΡ€ΠΎΠΏΡƒΡ‰Π΅Π½ %(count)s ΠΎΡ‚Π²Π΅Ρ‚. ΠžΡ‚ΠΊΡ€ΠΎΠΉΡ‚Π΅ Ρ‚Ρ€Π΅Π΄, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΡƒΠ²ΠΈΠ΄Π΅Ρ‚ΡŒ всС ΠΎΡ‚Π²Π΅Ρ‚Ρ‹."
201 201 msgstr[1] ""
202 202 "ΠŸΡ€ΠΎΠΏΡƒΡ‰Π΅Π½ΠΎ %(count)s ΠΎΡ‚Π²Π΅Ρ‚Π°. ΠžΡ‚ΠΊΡ€ΠΎΠΉΡ‚Π΅ Ρ‚Ρ€Π΅Π΄, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΡƒΠ²ΠΈΠ΄Π΅Ρ‚ΡŒ всС ΠΎΡ‚Π²Π΅Ρ‚Ρ‹."
203 203 msgstr[2] ""
204 204 "ΠŸΡ€ΠΎΠΏΡƒΡ‰Π΅Π½ΠΎ %(count)s ΠΎΡ‚Π²Π΅Ρ‚ΠΎΠ². ΠžΡ‚ΠΊΡ€ΠΎΠΉΡ‚Π΅ Ρ‚Ρ€Π΅Π΄, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΡƒΠ²ΠΈΠ΄Π΅Ρ‚ΡŒ всС ΠΎΡ‚Π²Π΅Ρ‚Ρ‹."
205 205
206 206 #: templates/boards/all_threads.html:127 templates/boards/feed.html:40
207 207 #: templates/boards/notifications.html:27 templates/search/search.html:37
208 208 msgid "Next page"
209 209 msgstr "Π‘Π»Π΅Π΄ΡƒΡŽΡ‰Π°Ρ страница"
210 210
211 211 #: templates/boards/all_threads.html:132
212 212 msgid "No threads exist. Create the first one!"
213 213 msgstr "НСт Ρ‚Π΅ΠΌ. Π‘ΠΎΠ·Π΄Π°ΠΉΡ‚Π΅ ΠΏΠ΅Ρ€Π²ΡƒΡŽ!"
214 214
215 215 #: templates/boards/all_threads.html:138
216 216 msgid "Create new thread"
217 217 msgstr "Π‘ΠΎΠ·Π΄Π°Ρ‚ΡŒ Π½ΠΎΠ²ΡƒΡŽ Ρ‚Π΅ΠΌΡƒ"
218 218
219 219 #: templates/boards/all_threads.html:143 templates/boards/preview.html:16
220 220 #: templates/boards/thread_normal.html:51
221 221 msgid "Post"
222 222 msgstr "ΠžΡ‚ΠΏΡ€Π°Π²ΠΈΡ‚ΡŒ"
223 223
224 224 #: templates/boards/all_threads.html:144 templates/boards/preview.html:6
225 225 #: templates/boards/staticpages/help.html:21
226 226 #: templates/boards/thread_normal.html:52
227 227 msgid "Preview"
228 228 msgstr "ΠŸΡ€Π΅Π΄ΠΏΡ€ΠΎΡΠΌΠΎΡ‚Ρ€"
229 229
230 230 #: templates/boards/all_threads.html:149
231 231 msgid "Tags must be delimited by spaces. Text or image is required."
232 232 msgstr ""
233 233 "ΠœΠ΅Ρ‚ΠΊΠΈ Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ Ρ€Π°Π·Π΄Π΅Π»Π΅Π½Ρ‹ ΠΏΡ€ΠΎΠ±Π΅Π»Π°ΠΌΠΈ. ВСкст ΠΈΠ»ΠΈ ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ ΠΎΠ±ΡΠ·Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹."
234 234
235 235 #: templates/boards/all_threads.html:152 templates/boards/thread_normal.html:58
236 236 msgid "Text syntax"
237 237 msgstr "Бинтаксис тСкста"
238 238
239 239 #: templates/boards/all_threads.html:166 templates/boards/feed.html:53
240 240 msgid "Pages:"
241 241 msgstr "Π‘Ρ‚Ρ€Π°Π½ΠΈΡ†Ρ‹: "
242 242
243 243 #: templates/boards/authors.html:6 templates/boards/authors.html.py:12
244 244 msgid "Authors"
245 245 msgstr "Авторы"
246 246
247 247 #: templates/boards/authors.html:26
248 248 msgid "Distributed under the"
249 249 msgstr "РаспространяСтся ΠΏΠΎΠ΄"
250 250
251 251 #: templates/boards/authors.html:28
252 252 msgid "license"
253 253 msgstr "Π»ΠΈΡ†Π΅Π½Π·ΠΈΠ΅ΠΉ"
254 254
255 255 #: templates/boards/authors.html:30
256 256 msgid "Repository"
257 257 msgstr "Π Π΅ΠΏΠΎΠ·ΠΈΡ‚ΠΎΡ€ΠΈΠΉ"
258 258
259 259 #: templates/boards/base.html:14 templates/boards/base.html.py:41
260 260 msgid "Feed"
261 261 msgstr "Π›Π΅Π½Ρ‚Π°"
262 262
263 263 #: templates/boards/base.html:31
264 264 msgid "All threads"
265 265 msgstr "ВсС Ρ‚Π΅ΠΌΡ‹"
266 266
267 267 #: templates/boards/base.html:37
268 268 msgid "Add tags"
269 269 msgstr "Π”ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ ΠΌΠ΅Ρ‚ΠΊΠΈ"
270 270
271 271 #: templates/boards/base.html:39
272 272 msgid "Tag management"
273 273 msgstr "Π£ΠΏΡ€Π°Π²Π»Π΅Π½ΠΈΠ΅ ΠΌΠ΅Ρ‚ΠΊΠ°ΠΌΠΈ"
274 274
275 275 #: templates/boards/base.html:39
276 276 msgid "tags"
277 277 msgstr "ΠΌΠ΅Ρ‚ΠΊΠΈ"
278 278
279 279 #: templates/boards/base.html:40
280 280 msgid "search"
281 281 msgstr "поиск"
282 282
283 283 #: templates/boards/base.html:41 templates/boards/feed.html:11
284 284 msgid "feed"
285 285 msgstr "Π»Π΅Π½Ρ‚Π°"
286 286
287 287 #: templates/boards/base.html:42 templates/boards/random.html:6
288 288 msgid "Random images"
289 289 msgstr "Π‘Π»ΡƒΡ‡Π°ΠΉΠ½Ρ‹Π΅ изобраТСния"
290 290
291 291 #: templates/boards/base.html:42
292 292 msgid "random"
293 293 msgstr "случайныС"
294 294
295 295 #: templates/boards/base.html:44
296 296 msgid "favorites"
297 297 msgstr "ΠΈΠ·Π±Ρ€Π°Π½Π½ΠΎΠ΅"
298 298
299 299 #: templates/boards/base.html:48 templates/boards/base.html.py:49
300 300 #: templates/boards/notifications.html:8
301 301 msgid "Notifications"
302 302 msgstr "УвСдомлСния"
303 303
304 304 #: templates/boards/base.html:56 templates/boards/settings.html:8
305 305 msgid "Settings"
306 306 msgstr "Настройки"
307 307
308 308 #: templates/boards/base.html:59
309 309 msgid "Loading..."
310 310 msgstr "Π—Π°Π³Ρ€ΡƒΠ·ΠΊΠ°..."
311 311
312 312 #: templates/boards/base.html:71
313 313 msgid "Admin"
314 314 msgstr "АдминистрированиС"
315 315
316 316 #: templates/boards/base.html:73
317 317 #, python-format
318 318 msgid "Speed: %(ppd)s posts per day"
319 319 msgstr "Π‘ΠΊΠΎΡ€ΠΎΡΡ‚ΡŒ: %(ppd)s сообщСний Π² дСнь"
320 320
321 321 #: templates/boards/base.html:75
322 322 msgid "Up"
323 323 msgstr "Π’Π²Π΅Ρ€Ρ…"
324 324
325 325 #: templates/boards/feed.html:45
326 326 msgid "No posts exist. Create the first one!"
327 327 msgstr "НСт сообщСний. Π‘ΠΎΠ·Π΄Π°ΠΉΡ‚Π΅ ΠΏΠ΅Ρ€Π²ΠΎΠ΅!"
328 328
329 329 #: templates/boards/post.html:33
330 330 msgid "Open"
331 331 msgstr "ΠžΡ‚ΠΊΡ€Ρ‹Ρ‚ΡŒ"
332 332
333 333 #: templates/boards/post.html:35 templates/boards/post.html.py:46
334 334 msgid "Reply"
335 335 msgstr "ΠžΡ‚Π²Π΅Ρ‚ΠΈΡ‚ΡŒ"
336 336
337 337 #: templates/boards/post.html:41
338 338 msgid " in "
339 339 msgstr " Π² "
340 340
341 341 #: templates/boards/post.html:51
342 342 msgid "Edit"
343 343 msgstr "Π˜Π·ΠΌΠ΅Π½ΠΈΡ‚ΡŒ"
344 344
345 345 #: templates/boards/post.html:53
346 346 msgid "Edit thread"
347 347 msgstr "Π˜Π·ΠΌΠ΅Π½ΠΈΡ‚ΡŒ Ρ‚Π΅ΠΌΡƒ"
348 348
349 349 #: templates/boards/post.html:91
350 350 msgid "Replies"
351 351 msgstr "ΠžΡ‚Π²Π΅Ρ‚Ρ‹"
352 352
353 353 #: templates/boards/post.html:103
354 354 #, python-format
355 355 msgid "%(count)s image"
356 356 msgid_plural "%(count)s images"
357 357 msgstr[0] "%(count)s ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅"
358 358 msgstr[1] "%(count)s изобраТСния"
359 359 msgstr[2] "%(count)s ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ"
360 360
361 361 #: templates/boards/rss/post.html:5
362 362 msgid "Post image"
363 363 msgstr "Π˜Π·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅ сообщСния"
364 364
365 365 #: templates/boards/settings.html:15
366 366 msgid "You are moderator."
367 367 msgstr "Π’Ρ‹ ΠΌΠΎΠ΄Π΅Ρ€Π°Ρ‚ΠΎΡ€."
368 368
369 369 #: templates/boards/settings.html:19
370 370 msgid "Hidden tags:"
371 371 msgstr "Π‘ΠΊΡ€Ρ‹Ρ‚Ρ‹Π΅ ΠΌΠ΅Ρ‚ΠΊΠΈ:"
372 372
373 373 #: templates/boards/settings.html:25
374 374 msgid "No hidden tags."
375 375 msgstr "НСт скрытых ΠΌΠ΅Ρ‚ΠΎΠΊ."
376 376
377 377 #: templates/boards/settings.html:34
378 378 msgid "Save"
379 379 msgstr "Π‘ΠΎΡ…Ρ€Π°Π½ΠΈΡ‚ΡŒ"
380 380
381 381 #: templates/boards/staticpages/banned.html:6
382 382 msgid "Banned"
383 383 msgstr "Π—Π°Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²Π°Π½"
384 384
385 385 #: templates/boards/staticpages/banned.html:11
386 386 msgid "Your IP address has been banned. Contact the administrator"
387 387 msgstr "Π’Π°Ρˆ IP адрСс Π±Ρ‹Π» Π·Π°Π±Π»ΠΎΠΊΠΈΡ€ΠΎΠ²Π°Π½. Π‘Π²ΡΠΆΠΈΡ‚Π΅ΡΡŒ с администратором"
388 388
389 389 #: templates/boards/staticpages/help.html:6
390 390 #: templates/boards/staticpages/help.html:10
391 391 msgid "Syntax"
392 392 msgstr "Бинтаксис"
393 393
394 394 #: templates/boards/staticpages/help.html:11
395 395 msgid "Italic text"
396 396 msgstr "ΠšΡƒΡ€ΡΠΈΠ²Π½Ρ‹ΠΉ тСкст"
397 397
398 398 #: templates/boards/staticpages/help.html:12
399 399 msgid "Bold text"
400 400 msgstr "ΠŸΠΎΠ»ΡƒΠΆΠΈΡ€Π½Ρ‹ΠΉ тСкст"
401 401
402 402 #: templates/boards/staticpages/help.html:13
403 403 msgid "Spoiler"
404 404 msgstr "Π‘ΠΏΠΎΠΉΠ»Π΅Ρ€"
405 405
406 406 #: templates/boards/staticpages/help.html:14
407 407 msgid "Link to a post"
408 408 msgstr "Бсылка Π½Π° сообщСниС"
409 409
410 410 #: templates/boards/staticpages/help.html:15
411 411 msgid "Strikethrough text"
412 412 msgstr "Π—Π°Ρ‡Π΅Ρ€ΠΊΠ½ΡƒΡ‚Ρ‹ΠΉ тСкст"
413 413
414 414 #: templates/boards/staticpages/help.html:16
415 415 msgid "Comment"
416 416 msgstr "ΠšΠΎΠΌΠΌΠ΅Π½Ρ‚Π°Ρ€ΠΈΠΉ"
417 417
418 418 #: templates/boards/staticpages/help.html:17
419 419 #: templates/boards/staticpages/help.html:18
420 420 msgid "Quote"
421 421 msgstr "Π¦ΠΈΡ‚Π°Ρ‚Π°"
422 422
423 423 #: templates/boards/staticpages/help.html:21
424 424 msgid "You can try pasting the text and previewing the result here:"
425 425 msgstr "Π’Ρ‹ ΠΌΠΎΠΆΠ΅Ρ‚Π΅ ΠΏΠΎΠΏΡ€ΠΎΠ±ΠΎΠ²Π°Ρ‚ΡŒ Π²ΡΡ‚Π°Π²ΠΈΡ‚ΡŒ тСкст ΠΈ ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΈΡ‚ΡŒ Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚ здСсь:"
426 426
427 427 #: templates/boards/tags.html:17
428 428 msgid "Sections:"
429 429 msgstr "Π Π°Π·Π΄Π΅Π»Ρ‹:"
430 430
431 431 #: templates/boards/tags.html:30
432 432 msgid "Other tags:"
433 433 msgstr "Π”Ρ€ΡƒΠ³ΠΈΠ΅ ΠΌΠ΅Ρ‚ΠΊΠΈ:"
434 434
435 435 #: templates/boards/tags.html:43
436 436 msgid "All tags..."
437 437 msgstr "ВсС ΠΌΠ΅Ρ‚ΠΊΠΈ..."
438 438
439 439 #: templates/boards/thread.html:14
440 440 msgid "Normal"
441 441 msgstr "ΠΠΎΡ€ΠΌΠ°Π»ΡŒΠ½Ρ‹ΠΉ"
442 442
443 443 #: templates/boards/thread.html:15
444 444 msgid "Gallery"
445 445 msgstr "ГалСрСя"
446 446
447 447 #: templates/boards/thread.html:16
448 448 msgid "Tree"
449 449 msgstr "Π”Π΅Ρ€Π΅Π²ΠΎ"
450 450
451 451 #: templates/boards/thread.html:35
452 452 msgid "message"
453 453 msgid_plural "messages"
454 454 msgstr[0] "сообщСниС"
455 455 msgstr[1] "сообщСния"
456 456 msgstr[2] "сообщСний"
457 457
458 458 #: templates/boards/thread.html:38
459 459 msgid "image"
460 460 msgid_plural "images"
461 461 msgstr[0] "ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠ΅"
462 462 msgstr[1] "изобраТСния"
463 463 msgstr[2] "ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ"
464 464
465 465 #: templates/boards/thread.html:40
466 466 msgid "Last update: "
467 467 msgstr "ПослСднСС обновлСниС: "
468 468
469 469 #: templates/boards/thread_gallery.html:36
470 470 msgid "No images."
471 471 msgstr "НСт ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ."
472 472
473 473 #: templates/boards/thread_normal.html:30
474 474 msgid "posts to bumplimit"
475 475 msgstr "сообщСний Π΄ΠΎ Π±Π°ΠΌΠΏΠ»ΠΈΠΌΠΈΡ‚Π°"
476 476
477 477 #: templates/boards/thread_normal.html:44
478 478 msgid "Reply to thread"
479 479 msgstr "ΠžΡ‚Π²Π΅Ρ‚ΠΈΡ‚ΡŒ Π² Ρ‚Π΅ΠΌΡƒ"
480 480
481 481 #: templates/boards/thread_normal.html:44
482 482 msgid "to message "
483 483 msgstr "Π½Π° сообщСниС"
484 484
485 485 #: templates/boards/thread_normal.html:59
486 486 msgid "Close form"
487 487 msgstr "Π—Π°ΠΊΡ€Ρ‹Ρ‚ΡŒ Ρ„ΠΎΡ€ΠΌΡƒ"
488 488
489 489 #: templates/search/search.html:17
490 490 msgid "Ok"
491 491 msgstr "Ок"
492 492
493 493 #: utils.py:120
494 494 #, python-format
495 495 msgid "File must be less than %s but is %s."
496 496 msgstr "Π€Π°ΠΉΠ» Π΄ΠΎΠ»ΠΆΠ΅Π½ Π±Ρ‹Ρ‚ΡŒ ΠΌΠ΅Π½Π΅Π΅ %s, Π½ΠΎ Π΅Π³ΠΎ Ρ€Π°Π·ΠΌΠ΅Ρ€ %s."
497 497
498 498 msgid "Please wait %(delay)d second before sending message"
499 499 msgid_plural "Please wait %(delay)d seconds before sending message"
500 500 msgstr[0] "ΠŸΠΎΠΆΠ°Π»ΡƒΠΉΡΡ‚Π° ΠΏΠΎΠ΄ΠΎΠΆΠ΄ΠΈΡ‚Π΅ %(delay)d сСкунду ΠΏΠ΅Ρ€Π΅Π΄ ΠΎΡ‚ΠΏΡ€Π°Π²ΠΊΠΎΠΉ сообщСния"
501 501 msgstr[1] "ΠŸΠΎΠΆΠ°Π»ΡƒΠΉΡΡ‚Π° ΠΏΠΎΠ΄ΠΎΠΆΠ΄ΠΈΡ‚Π΅ %(delay)d сСкунды ΠΏΠ΅Ρ€Π΅Π΄ ΠΎΡ‚ΠΏΡ€Π°Π²ΠΊΠΎΠΉ сообщСния"
502 502 msgstr[2] "ΠŸΠΎΠΆΠ°Π»ΡƒΠΉΡΡ‚Π° ΠΏΠΎΠ΄ΠΎΠΆΠ΄ΠΈΡ‚Π΅ %(delay)d сСкунд ΠΏΠ΅Ρ€Π΅Π΄ ΠΎΡ‚ΠΏΡ€Π°Π²ΠΊΠΎΠΉ сообщСния"
503 503
504 504 msgid "New threads"
505 505 msgstr "НовыС Ρ‚Π΅ΠΌΡ‹"
506 506
507 507 #, python-format
508 508 msgid "Max file size is %(size)s."
509 509 msgstr "ΠœΠ°ΠΊΡΠΈΠΌΠ°Π»ΡŒΠ½Ρ‹ΠΉ Ρ€Π°Π·ΠΌΠ΅Ρ€ Ρ„Π°ΠΉΠ»Π° %(size)s."
510 510
511 511 msgid "Size of media:"
512 512 msgstr "Π Π°Π·ΠΌΠ΅Ρ€ ΠΌΠ΅Π΄ΠΈΠ°:"
513 513
514 514 msgid "Statistics"
515 515 msgstr "Бтатистика"
516 516
517 517 msgid "Invalid PoW."
518 518 msgstr "НСвСрный PoW."
519 519
520 520 msgid "Stale PoW."
521 521 msgstr "PoW устарСл."
522 522
523 523 msgid "Show"
524 524 msgstr "ΠŸΠΎΠΊΠ°Π·Ρ‹Π²Π°Ρ‚ΡŒ"
525 525
526 526 msgid "Hide"
527 527 msgstr "Π‘ΠΊΡ€Ρ‹Π²Π°Ρ‚ΡŒ"
528 528
529 529 msgid "Add to favorites"
530 530 msgstr "Π”ΠΎΠ±Π°Π²ΠΈΡ‚ΡŒ Π² ΠΈΠ·Π±Ρ€Π°Π½Π½ΠΎΠ΅"
531 531
532 532 msgid "Remove from favorites"
533 533 msgstr "Π£Π±Ρ€Π°Ρ‚ΡŒ ΠΈΠ· ΠΈΠ·Π±Ρ€Π°Π½Π½ΠΎΠ³ΠΎ"
534 534
535 535 msgid "Monochrome"
536 536 msgstr "ΠœΠΎΠ½ΠΎΡ…Ρ€ΠΎΠΌΠ½Ρ‹ΠΉ"
537 537
538 538 msgid "Subsections: "
539 539 msgstr "ΠŸΠΎΠ΄Ρ€Π°Π·Π΄Π΅Π»Ρ‹: "
540 540
541 541 msgid "Change file source"
542 542 msgstr "Π˜Π·ΠΌΠ΅Π½ΠΈΡ‚ΡŒ источник Ρ„Π°ΠΉΠ»Π°"
543 543
544 544 msgid "interesting"
545 545 msgstr "интСрСсноС"
546 546
547 547 msgid "images"
548 548 msgstr "изобраТСния"
549 549
550 550 msgid "Delete post"
551 551 msgstr "Π£Π΄Π°Π»ΠΈΡ‚ΡŒ пост"
552 552
553 553 msgid "Delete thread"
554 554 msgstr "Π£Π΄Π°Π»ΠΈΡ‚ΡŒ Ρ‚Π΅ΠΌΡƒ"
555 555
556 556 msgid "Messages per day/week/month:"
557 557 msgstr "Π‘ΠΎΠΎΠ±Ρ‰Π΅Π½ΠΈΠΉ Π·Π° дСнь/нСдСлю/мСсяц:"
558 558
559 559 msgid "Subscribe to thread"
560 560 msgstr "ΠŸΠΎΠ΄ΠΏΠΈΡΠ°Ρ‚ΡŒΡΡ Π½Π° Ρ‚Π΅ΠΌΡƒ"
561 561
562 562 msgid "Active threads:"
563 563 msgstr "АктивныС Ρ‚Π΅ΠΌΡ‹:"
564 564
565 565 msgid "No active threads today."
566 566 msgstr "БСгодня Π½Π΅Ρ‚ Π°ΠΊΡ‚ΠΈΠ²Π½Ρ‹Ρ… Ρ‚Π΅ΠΌ."
567 567
568 568 msgid "Insert URLs on separate lines."
569 569 msgstr "ВставляйтС ссылки Π½Π° ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½Ρ‹Ρ… строках."
570
571 msgid "You can post no more than %(files)d file."
572 msgid_plural "You can post no more than %(files)d files."
573 msgstr[0] "Π’Ρ‹ ΠΌΠΎΠΆΠ΅Ρ‚Π΅ ΠΎΡ‚ΠΏΡ€Π°Π²ΠΈΡ‚ΡŒ Π½Π΅ Π±ΠΎΠ»Π΅Π΅ %(files)d Ρ„Π°ΠΉΠ»Π°."
574 msgstr[1] "Π’Ρ‹ ΠΌΠΎΠΆΠ΅Ρ‚Π΅ ΠΎΡ‚ΠΏΡ€Π°Π²ΠΈΡ‚ΡŒ Π½Π΅ Π±ΠΎΠ»Π΅Π΅ %(files)d Ρ„Π°ΠΉΠ»ΠΎΠ²."
575 msgstr[2] "Π’Ρ‹ ΠΌΠΎΠΆΠ΅Ρ‚Π΅ ΠΎΡ‚ΠΏΡ€Π°Π²ΠΈΡ‚ΡŒ Π½Π΅ Π±ΠΎΠ»Π΅Π΅ %(files)d Ρ„Π°ΠΉΠ»ΠΎΠ²."
@@ -1,173 +1,190
1 1 var ITEM_FILE_SOURCE = 'fileSource';
2 var URL_STICKERS = '/api/stickers'
2 var URL_STICKERS = '/api/stickers';
3 3 var MIN_INPUT_LENGTH = 3;
4 var URL_DELIMITER = '\n';
4 5
5 6 $('input[name=image]').wrap($('<div class="file_wrap"></div>'));
6 7
7 8 $('body').on('change', 'input[name=image]', function(event) {
8 9 var file = event.target.files[0];
9 10
10 11 if(file.type.match('image.*')) {
11 12 var fileReader = new FileReader();
12 13
13 14 fileReader.addEventListener("load", function(event) {
14 15 var wrapper = $('.file_wrap');
15 16
16 17 wrapper.find('.file-thumb').remove();
17 18 wrapper.append(
18 19 $('<div class="file-thumb" style="background-image: url('+event.target.result+')"></div>')
19 20 );
20 21 });
21 22
22 23 fileReader.readAsDataURL(file);
23 24 }
24 25 });
25 26
26 27 var form = $('#form');
27 28 $('textarea').keypress(function(event) {
28 29 if ((event.which == 10 || event.which == 13) && event.ctrlKey) {
29 30 form.find('input[type=submit]').click();
30 31 }
31 32 });
32 33
33 34 $('#preview-button').click(function() {
34 35 var data = {
35 36 raw_text: $('textarea').val()
36 37 }
37 38
38 39 var diffUrl = '/api/preview/';
39 40
40 41 $.post(diffUrl,
41 42 data,
42 43 function(data) {
43 44 var previewTextBlock = $('#preview-text');
44 45 previewTextBlock.html(data);
45 46 previewTextBlock.show();
46 47
47 48 addScriptsToPost(previewTextBlock);
48 49 })
49 50 });
50 51
51 52 /**
52 53 * Show text in the errors row of the form.
53 54 * @param form
54 55 * @param text
55 56 */
56 57 function showAsErrors(form, text) {
57 58 form.children('.form-errors').remove();
58 59
59 60 if (text.length > 0) {
60 61 var errorList = $('<div class="form-errors">' + text + '<div>');
61 62 errorList.appendTo(form);
62 63 }
63 64 }
64 65
65 66 function addHiddenInput(form, name, value) {
66 67 form.find('input[name=' + name + ']').val(value);
67 68 }
68 69
69 70 function selectFileChoice() {
70 71 var file_input = $('#id_file');
71 72 var url_input = $('#id_file_url');
72 73
73 74 var file_input_row = file_input.parent().parent();
74 75 var url_input_row = url_input.parent().parent();
75 76
76 77 file_input_row.toggle();
77 78 url_input_row.toggle();
78 79 url_input.val('');
79 80 file_input.val('');
80 81
81 82 var source;
82 83 if (file_input_row.is(':visible')) {
83 84 source = 'file';
84 85 } else {
85 86 source = 'url';
86 87 }
87 88 localStorage.setItem(ITEM_FILE_SOURCE, source);
88 89 }
89 90
90 91 $(document).ready(function() {
91 92 var powDifficulty = parseInt($('body').attr('data-pow-difficulty'));
92 93 if (powDifficulty > 0 && typeof SharedWorker != 'undefined') {
93 94 var worker = new SharedWorker($('.post-form').attr('data-pow-script'));
94 95 worker.port.onmessage = function(e) {
95 96 var form = $('#form');
96 97 addHiddenInput(form, 'timestamp', e.data.timestamp);
97 98 addHiddenInput(form, 'iteration', e.data.iteration);
98 99 addHiddenInput(form, 'guess', e.data.guess);
99 100
100 101 form.submit();
101 102 $('.post-form-w').unblock();
102 103 };
103 104 worker.onerror = function(event){
104 105 throw new Error(event.message + " (" + event.filename + ":" + event.lineno + ")");
105 106 };
106 107 worker.port.start();
107 108
108 109 var form = $('#form');
109 110 var submitButton = form.find('input[type=submit]');
110 111 submitButton.click(function() {
111 112 showAsErrors(form, gettext('Computing PoW...'));
112 113 $('.post-form-w').block({ message: gettext('Computing PoW...') })
113 114
114 115 var msg = $('textarea').val().trim();
115 116
116 117 var data = {
117 118 msg: msg,
118 119 difficulty: parseInt($('body').attr('data-pow-difficulty')),
119 120 hasher: $('.post-form').attr('data-hasher')
120 121 };
121 122 worker.port.postMessage(data);
122 123
123 124 return false;
124 125 });
125 126 }
126 127
127 128 var $fileSourceButton = $('#file-source-button');
128 129 if (window.localStorage) {
129 130 var source = localStorage.getItem(ITEM_FILE_SOURCE);
130 131 if (source == null) {
131 132 source = 'file';
132 133 }
133 134 if (source == 'file') {
134 135 $('#id_file_url').parent().parent().hide();
135 136 } else {
136 137 $('#id_file').parent().parent().hide();
137 138 }
138 139
139 140 $fileSourceButton.click(function() {
140 141 selectFileChoice();
141 142 });
142 143 } else {
143 144 $fileSourceButton.hide();
144 145 }
145 146
146 $('#id_file_url').autocomplete({
147 // Stickers autocomplete
148 function split( val ) {
149 return val.split(URL_DELIMITER);
150 }
151
152 function extractLast( term ) {
153 return split(term).pop();
154 }
155
156 $('#id_file_1').autocomplete({
147 157 source: function( request, response ) {
148 158 $.getJSON(URL_STICKERS, {
149 term: request.term
159 term: extractLast( request.term )
150 160 }, response);
151 161 },
152 162 search: function() {
153 163 // custom minLength
154 var term = this.value;
164 var term = extractLast( this.value );
155 165 if (term.length < MIN_INPUT_LENGTH) {
156 166 return false;
157 167 }
158 168 },
159 169 focus: function() {
160 170 // prevent value inserted on focus
161 171 return false;
162 172 },
163 173 select: function( event, ui ) {
164 this.value = ui.item.alias;
174 var terms = split( this.value );
175 // remove the current input
176 terms.pop();
177 // add the selected item
178 terms.push( ui.item.alias );
179 // add placeholder to get the comma-and-space at the end
180 terms.push("");
181 this.value = terms.join(URL_DELIMITER);
165 182 return false;
166 183 }
167 184 })
168 185 .autocomplete( "instance" )._renderItem = function( ul, item ) {
169 186 return $( "<li>" )
170 187 .append( "<div>" + '<img src="' + item.thumb + '">' + '<br />' + item.alias + "</div>" )
171 188 .appendTo( ul );
172 189 };
173 190 });
General Comments 0
You need to be logged in to leave comments. Login now