1
0
mirror of https://github.com/vichan-devel/vichan.git synced 2024-11-28 17:31:00 +01:00
This commit is contained in:
Fredrick Brennan 2015-03-20 12:10:17 +08:00
commit 32eb90add7
18 changed files with 372 additions and 78 deletions

View File

@ -1,35 +1,17 @@
<?php
include "inc/functions.php";
include "inc/lib/ayah/ayah.php";
$cbRecaptcha = false;
//don't load recaptcha LIB unless its enabled!
if ($config['cbRecaptcha']){
$cbRecaptcha = true;
include "inc/lib/recaptcha/recaptchalib.php";
}
checkBan('*');
$ayah = (($config['ayah_enabled']) ? new AYAH() : false);
if (!isset($_POST['uri'], $_POST['title'], $_POST['subtitle'], $_POST['username'], $_POST['password'])) {
if (!$ayah){
$game_html = '';
} else {
$game_html = '<tr><th>'._('Game').'</th><td>' . $ayah->getPublisherHTML() . '</td></tr>';
}
if (!$cbRecaptcha){
$recapcha_html = '';
} else {
$recapcha_html = '<tr><th>reCaptcha</th><td>' . recaptcha_get_html($config['recaptcha_public'], NULL, TRUE) . '</td></tr>';
}
include '8chan-captcha/functions.php';
$password = base64_encode(openssl_random_pseudo_bytes(9));
$body = Element("8chan/create.html", array("config" => $config, "password" => $password, "game_html" => $game_html, "recapcha_html" => $recapcha_html));
$captcha = generate_captcha($config['captcha']['extra']);
$body = Element("8chan/create.html", array("config" => $config, "password" => $password, "captcha" => $captcha));
echo Element("page.html", array("config" => $config, "body" => $body, "title" => _("Create your board"), "subtitle" => _("before someone else does")));
}
@ -41,26 +23,13 @@ $subtitle = $_POST['subtitle'];
$username = $_POST['username'];
$password = $_POST['password'];
$resp = ($cbRecaptcha) ? recaptcha_check_answer ($config['recaptcha_private'],
$_SERVER["REMOTE_ADDR"],
$_POST["recaptcha_challenge_field"],
$_POST["recaptcha_response_field"]):false;
$resp = file_get_contents($config['captcha']['provider_check'] . "?" . http_build_query([
'mode' => 'check',
'text' => $_POST['captcha_text'],
'extra' => $config['captcha']['extra'],
'cookie' => $_POST['captcha_cookie']
]));
if ($resp != false){
$passedCaptcha = $resp->is_valid;
} else {
$passedCaptcha = true;
}
if (!$ayah){
$score = true;
} else {
$score = $ayah->scoreResult();
}
if (!$score)
error(_('You failed the game'));
if (!$passedCaptcha)
error(_('You failed to enter the reCaptcha correctly'));
if (!preg_match('/^[a-z0-9]{1,30}$/', $uri))
error(_('Invalid URI'));
if (!(strlen($title) < 40))
@ -69,6 +38,8 @@ if (!(strlen($subtitle) < 200))
error(_('Invalid subtitle'));
if (!preg_match('/^[a-zA-Z0-9._]{1,30}$/', $username))
error(_('Invalid username'));
if ($resp !== '1')
error($config['error']['captcha']);
foreach (listBoards() as $i => $board) {
if ($board['uri'] == $uri)

View File

@ -1,31 +1,26 @@
<?php
include 'inc/functions.php';
include "inc/lib/recaptcha/recaptchalib.php";
require_once 'Net/DNS2.php';
checkBan('*');
// My nameserver was broken and I couldn't edit resolv.conf so I just did this instead
$dns = new Net_DNS2_Resolver(array('nameservers' => array('8.8.8.8')));
$result = $dns->query(RECAPTCHA_VERIFY_SERVER, "A");
if ($result and $result->answer[0]) {
$RECAPTCHA_VERIFY_SERVER_IP = $result->answer[0]->address;
} else {
$RECAPTCHA_VERIFY_SERVER_IP = RECAPTCHA_VERIFY_SERVER;
}
include '8chan-captcha/functions.php';
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$ayah_html = recaptcha_get_html($config['recaptcha_public'], NULL, TRUE);
$body = Element("8chan/dnsbls.html", array("config" => $config, "ayah_html" => $ayah_html));
$captcha = generate_captcha($config['captcha']['extra']);
$html = "{$captcha['html']}<br/>
<input class='captcha_text' name='captcha_text' size='25' maxlength='6' autocomplete='off' type='text'>
<input class='captcha_cookie' name='captcha_cookie' type='hidden' autocomplete='off' value='{$captcha['cookie']}'><br/>";
$body = Element("8chan/dnsbls.html", array("config" => $config, "ayah_html" => $html));
echo Element("page.html", array("config" => $config, "body" => $body, "title" => _("Bypass DNSBL"), "subtitle" => _("Post even if blocked")));
} else {
$score = recaptcha_check_answer($config['recaptcha_private'],
$_SERVER["REMOTE_ADDR"],
$_POST["recaptcha_challenge_field"],
$_POST["recaptcha_response_field"],
array(),
$RECAPTCHA_VERIFY_SERVER_IP);
$resp = file_get_contents($config['captcha']['provider_check'] . "?" . http_build_query([
'mode' => 'check',
'text' => $_POST['captcha_text'],
'extra' => $config['captcha']['extra'],
'cookie' => $_POST['captcha_cookie']
]));
if ($score->is_valid) {
if ($resp === '1') {
$tor = checkDNSBL($_SERVER['REMOTE_ADDR']);
if (!$tor) {
$query = prepare('INSERT INTO ``dnsbl_bypass`` VALUES(:ip, NOW()) ON DUPLICATE KEY UPDATE `created`=NOW()');

View File

@ -143,6 +143,13 @@ Assuming the /b/ board, they are as follows:</p>
<p>Just read the data to get an idea of what is exposed and under what attribute names. It should be self explanatory.</p>
<p><strong>Endpoints not listed here, like post.php, catalog.json or boards-top20.json are subject to change or removal at any time!</strong></p>
<h2>I would like to contribute a translation in my language.</h2>
<p>Great! See <a href="/translation.html">this page</a> for more information.</p>
<h2>Are there any publicly available statistics?</h2>
<p>Yes, take a look at <a href="http://stats.4ch.net/8chan/">http://stats.4ch.net/8chan/</a>.
<h2>I got an email from an @8chan.co email address, is that you?</h2>
<p>8chan.co uses <a href="https://cock.li">cock.li</a> to manage our domain's email. cock.li allows anyone to create an email account @8chan.co.</p>
<p>That said, we have quite a few official 8chan.co email addresses. They are:</p>

View File

@ -1018,7 +1018,8 @@
// Width and height (and more?) of post flags. Can be overridden with the Tinyboard post modifier:
// <tinyboard flag style>.
$config['flag_style'] = 'width:16px;height:11px;';
// $config['flag_style'] = 'width:16px;height:11px;';
$config['flag_style'] = '';
/*
* ====================

View File

@ -142,7 +142,7 @@
$config['additional_javascript'][] = 'js/image-hover.js';
$config['additional_javascript'][] = 'js/auto-scroll.js';
$config['additional_javascript'][] = 'js/twemoji/twemoji.js';
$config['additional_javascript'][] = 'js/multi-image.js';
$config['additional_javascript'][] = 'js/file-selector.js';
// Oekaki (now depends on config.oekaki so can be in all scripts)
$config['additional_javascript'][] = 'js/jquery-ui.custom.min.js';
$config['additional_javascript'][] = 'js/wPaint/lib/wColorPicker.min.js';

View File

@ -287,8 +287,6 @@ $(document).ready(function(){
};
$(window).scroll(function() {
recheck_activated();
// if the newest post is not visible
if($(this).scrollTop() + $(this).height() <
$('div.post:last').position().top + $('div.post:last').height()) {

183
js/file-selector.js Normal file
View File

@ -0,0 +1,183 @@
/*
* file-selector.js - Add support for drag and drop file selection, and paste from clipbboard on supported browsers.
*
* Usage:
* $config['additional_javascript'][] = 'js/jquery.min.js';
* $config['additional_javascript'][] = 'js/file-selector.js';
*/
function init_file_selector(max_images) {
$(document).ready(function () {
// add options panel item
if (window.Options && Options.get_tab('general')) {
Options.extend_tab('general', '<label id="file-drag-drop"><input type="checkbox">' + _('Drag and drop file selection') + '</label>');
$('#file-drag-drop>input').on('click', function() {
if ($('#file-drag-drop>input').is(':checked')) {
localStorage.file_dragdrop = 'true';
} else {
localStorage.file_dragdrop = 'false';
}
});
if (typeof localStorage.file_dragdrop === 'undefined') localStorage.file_dragdrop = 'true';
if (localStorage.file_dragdrop === 'true') $('#file-drag-drop>input').prop('checked', true);
}
});
// disabled by user, or incompatible browser.
if (localStorage.file_dragdrop == 'false' || !(window.URL.createObjectURL && window.File))
return;
// multipost not enabled
if (typeof max_images == 'undefined') {
var max_images = 1;
}
var files = [];
$('#upload_file').hide(); // hide the original file selector
$('.dropzone-wrap').css('user-select', 'none').show(); // let jquery add browser specific prefix
function addFile(file) {
if (files.length == max_images)
return;
files.push(file);
addThumb(file);
}
function removeFile(file) {
files.splice(files.indexOf(file), 1);
}
function getThumbElement(file) {
return $('.tmb-container').filter(function(){return($(this).data('file-ref')==file);});
}
function addThumb(file) {
var fileName = (file.name.length < 24) ? file.name : file.name.substr(0, 22) + '…';
var fileType = file.type.split('/')[0];
var fileExt = file.type.split('/')[1];
var $container = $('<div>');
$container
.addClass('tmb-container')
.data('file-ref', file)
.append(
$('<div>').addClass('remove-btn').html('✖'),
$('<div>').addClass('file-tmb'),
$('<div>').addClass('tmb-filename').html(fileName)
)
.appendTo($('.file-thumbs'));
var $fileThumb = $container.find('.file-tmb');
if (fileType == 'image') {
// if image file, generate thumbnail
var objURL = window.URL.createObjectURL(file);
$fileThumb.css('background-image', 'url('+ objURL +')');
} else {
$fileThumb.html('<span>' + fileExt.toUpperCase() + '</span>');
}
}
$(document).on('ajax_before_post', function (e, formData) {
for (var i=0; i<max_images; i++) {
var key = 'file';
if (i > 0) key += i + 1;
formData.append(key, files[i]);
}
});
// clear file queue and UI on success
$(document).on('ajax_after_post', function () {
files = [];
$('.file-thumbs').empty();
});
var dragCounter = 0;
var dropHandlers = {
dragenter: function (e) {
e.stopPropagation();
e.preventDefault();
if (dragCounter === 0) $(this).addClass('dragover');
dragCounter++;
},
dragover: function (e) {
// needed for webkit to work
e.stopPropagation();
e.preventDefault();
},
dragleave: function (e) {
e.stopPropagation();
e.preventDefault();
dragCounter--;
if (dragCounter === 0) $(this).removeClass('dragover');
},
drop: function (e) {
e.stopPropagation();
e.preventDefault();
$(this).removeClass('dragover');
dragCounter = 0;
var fileList = e.originalEvent.dataTransfer.files;
for (var i=0; i<fileList.length; i++) {
addFile(fileList[i]);
}
}
};
// attach handlers
$(document).on(dropHandlers, '.dropzone');
$(document).on('click', '.dropzone .remove-btn', function (e) {
e.stopPropagation();
var file = $(e.target).parent().data('file-ref');
getThumbElement(file).remove();
removeFile(file);
});
$(document).on('keypress click', '.dropzone, .dropzone .file-hint', function (e) {
e.stopPropagation();
// accept mosue click or Enter
if (e.which != 1 && e.which != 13)
return;
var $fileSelector = $('<input type="file" multiple>');
$fileSelector.on('change', function (e) {
if (this.files.length > 0) {
for (var i=0; i<this.files.length; i++) {
addFile(this.files[i]);
}
}
$(this).remove();
});
$fileSelector.click();
});
$(document).on('paste', function (e) {
var clipboard = e.originalEvent.clipboardData;
if (typeof clipboard.items != 'undefined' && clipboard.items.length != 0) {
//Webkit
for (var i=0; i<clipboard.items.length; i++) {
if (clipboard.items[i].kind != 'file')
continue;
//convert blob to file
var file = new File([clipboard.items[i].getAsFile()], 'ClipboardImage.png', {type: 'image/png'});
addFile(file);
}
}
});
}

View File

@ -140,13 +140,21 @@ function imageHoverStart(e) { //Pashe, anonish, WTFPL
if (isVideo(getFileExtension(fullUrl))) {return;}
hoverImage = $('<img id="chx_hoverImage" src="'+fullUrl+'" />');
if (getSetting("imageHoverFollowCursor")) {
var size = $this.parents('.file').find('.unimportant').text().match(/\b(\d+)x(\d+)\b/),
maxWidth = $(window).width(),
maxHeight = $(window).height();
var scale = Math.min(1, maxWidth / size[1], maxHeight / size[2]);
hoverImage.css({
"position" : "absolute",
"z-index" : 101,
"pointer-events": "none",
"max-width" : $(window).width(),
"max-height" : $(window).height(),
"width" : size[1] + "px",
"height" : size[2] + "px",
"max-width" : (size[1] * scale) + "px",
"max-height" : (size[2] * scale) + "px",
'left' : e.pageX,
'top' : imgTop,
});

View File

@ -425,7 +425,7 @@ if (active_page === 'thread' || active_page === 'index' || active_page === 'cata
array = $post.find('.body').contents().filter(function () {if ($(this).text() !== '') return true;}).toArray();
array = $.map(array, function (ele) {
return $(ele).text();
return $(ele).text().trim();
});
comment = array.join(' ');

Binary file not shown.

Before

Width:  |  Height:  |  Size: 355 B

After

Width:  |  Height:  |  Size: 618 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 84 KiB

View File

@ -1181,3 +1181,81 @@ div.mix {
#youtube-size input {
width: 50px;
}
/* File selector */
.dropzone {
color: #000;
cursor: default;
margin: auto;
padding: 0px 4px;
text-align: center;
min-height: 50px;
max-height: 140px;
transition: 0.2s;
background-color: rgba(200, 200, 200, 0.5);
overflow-y: auto;
}
.dropzone-wrap {
width: 100%;
}
.dropzone .file-hint {
color: rgba(0, 0, 0, 0.5);
cursor: pointer;
position: relative;
margin-bottom: 5px;
padding: 10px 0px;
top: 5px;
transition: 0.2s;
border: 2px dashed rgba(125, 125, 125, 0.4);
}
.file-hint:hover, .dropzone.dragover .file-hint {
color: rgba(0, 0, 0, 1);
border-color: rgba(125, 125, 125, 0.8);
}
.dropzone.dragover {
background-color: rgba(200, 200, 200, 1);
}
.dropzone .file-thumbs {
text-align: left;
width: 100%;
}
.dropzone .tmb-container {
padding: 3px;
overflow-x: hidden;
white-space: nowrap;
}
.dropzone .file-tmb {
height: 40px;
width: 70px;
cursor: pointer;
display: inline-block;
text-align: center;
background-color: rgba(187, 187, 187, 0.5);
background-size: cover;
background-position: center;
}
.dropzone .file-tmb span {
font-weight: 600;
position: relative;
top: 13px;
}
.dropzone .tmb-filename {
display: inline-block;
vertical-align: bottom;
bottom: 12px;
position: relative;
margin-left: 5px;
}
.dropzone .remove-btn {
cursor: pointer;
color: rgba(125, 125, 125, 0.5);
display: inline-block;
vertical-align: bottom;
bottom: 10px;
position: relative;
margin-right: 5px;
font-size: 20px
}
.dropzone .remove-btn:hover {
color: rgba(125, 125, 125, 1);
}

View File

@ -7,8 +7,9 @@
<tr><th>Subtitle</th><td><input name="subtitle" type="text"> <span class="unimportant">{% trans %}(must be < 200 chars){% endtrans %}</td></tr>
<tr><th>{% trans %}Username{% endtrans %}</th><td><input name="username" type="text"> <span class="unimportant">{% trans %}(must contain only alphanumeric, periods and underscores){% endtrans %}</span></td></tr>
<tr><th>{% trans %}Password{% endtrans %}</th><td><input name="password" type="text" value="{{ password }}" readonly> <span class="unimportant">{% trans %}(write this down){% endtrans %}</span></td></tr>
{{ game_html }}
{{ recapcha_html }}
<tr><th>{% trans %}CAPTCHA{% endtrans %}</th><td>{{ captcha['html'] }}<br/>
<input class="captcha_text" name="captcha_text" size="25" maxlength="6" autocomplete="off" type="text">
<input class="captcha_cookie" name="captcha_cookie" type="hidden" autocomplete="off" value="{{ captcha['cookie']|e }}"><br/></td></tr>
</tbody>
</table>
<ul style="padding:0;text-align:center;list-style:none"><li><input type="submit" value="{% trans %}Create board{% endtrans %}"></li></ul>

View File

@ -1,5 +1,6 @@
<form method="POST">
<p>{% trans %}Your IP is listed in our DNSBL. To stop attackers, we require users who use certain IP ranges to pass a test which proves they are human every 24 hours.{% endtrans %}</p>
<p>{% trans %}Tor users need to fill out the CAPTCHA every 3 hours or 5 posts.{% endtrans %}</p>
<p>{% trans %}It is also possible that the site is currently under attack and we are requiring everyone to pass the test right now. Sorry for the inconvenience.{% endtrans %}</p>

View File

@ -336,7 +336,7 @@ function init() {
{% endraw %}
{% if config.allow_delete %}
if (document.forms.postcontrols) {
if (document.forms.postcontrols && document.forms.postcontrols.password) {
document.forms.postcontrols.password.value = localStorage.password;
}
{% endif %}

View File

@ -53,7 +53,7 @@
<tr><th>{% trans %}Public action log{% endtrans %}<br><span class="unimportant">{% trans %}Displays all actions to the public{% endtrans %}</span></th><td><select name="public_logs"><option value="0" {% if board.public_logs == 0 %}selected{% endif %}>None</option><option value="1" {% if board.public_logs == 1 %}selected{% endif %}>Full log of all actions</option><option value="2" {% if board.public_logs == 2 %}selected{% endif %}>Full log of all actions, no usernames</option></td></tr>
<tr><th>{% trans %}Page count{% endtrans %}</th><td><select name="max_pages">{% for i in range(2, 25) %}<option value="{{ i }}" {% if config.max_pages == i %}selected{% endif %}>{{ i }}</option>{% endfor %}</select></td></tr>
<tr><th>{% trans %}Bump limit{% endtrans %}</th><td><select name="reply_limit">{% for i in range(250, 750, 25) %}<option value="{{ i }}" {% if config.reply_limit == i %}selected{% endif %}>{{ i }}</option>{% endfor %}</select></td></tr>
<tr><th>{% trans %}Language{% endtrans %}<br/><span class="unimportant">{% trans %}To contribute translations, register at <a href="https://www.transifex.com/projects/p/tinyboard-vichan-devel/">Transifex</a>{% endtrans %}</span></th><td>
<tr><th>{% trans %}Language{% endtrans %}<br/><span class="unimportant">{% trans %}Read this page for more information about contributing translations:<br><a href="/translation.html">Translation tutorial</a> or use <a href="https://www.transifex.com/projects/p/infinity/">Transifex</a>{% endtrans %}</span></th><td>
<select name="locale">
<option value="en" {% if "en" == config.locale %}selected{% endif %}>en</option>
{% for language in languages %}

View File

@ -7,7 +7,7 @@
<input type="hidden" name="page" value="{{ current_page }}">
{% endif %}
{% if mod %}<input type="hidden" name="mod" value="1">{% endif %}
<table class="post-table">
<table class="post-table"><tbody>
{% if not config.field_disable_name or (mod and post.mod|hasPermission(config.mod.bypass_field_disable, board.uri)) %}<tr>
<th>
{% trans %}Name{% endtrans %}
@ -91,6 +91,13 @@
</th>
<td>
<input type="file" name="file" id="upload_file">
<div class="dropzone-wrap" style="display: none;">
<div class="dropzone" tabindex="0">
<div class="file-hint">Select/drop/paste files here</div>
<div class="file-thumbs"></div>
</div>
</div>
<script type="text/javascript">init_file_selector({{ config.max_images }});</script>
{% if config.allow_upload_by_url %}
<div style="float:none;text-align:left" id="upload_url">
<label for="file_url">{% trans %}Or URL{% endtrans %}</label>:
@ -129,7 +136,8 @@
<strong class="faq-message unimportant hint"><br />Confused? See the <a href="/faq.html">FAQ</a>.</strong>
</td>
</tr>
</tbody></table><table class="post-table-options"><tbody>
</tbody></table>
<table class="post-table-options"><tbody>
{% if not config.force_flag %}
{{ flag_tr }}
@ -163,17 +171,23 @@
<td>
<div class="no-bump-option">
<label><input title="No bump" id="no-bump" name="no-bump" type="checkbox">
{% trans %}Do not bump{% endtrans %} <span class="unimportant hint">{% trans %}(you can also write sage in the email field){% endtrans %}</span></label>
{% trans %}Do not bump{% endtrans %}
<br/>
<span class="unimportant hint">{% trans %}(you can also write sage in the email field){% endtrans %}</span></label>
</div>
{% if config.spoiler_images %}<div class="spoiler-images-option">
<label><input title="Spoiler images" id="spoiler" name="spoiler" type="checkbox">
{% trans %}Spoiler images{% endtrans %} <span class="unimportant hint">{% trans %}(this replaces the thumbnails of your images with question marks){% endtrans %}</label>
{% trans %}Spoiler images{% endtrans %}
<br/>
<span class="unimportant hint">{% trans %}(this replaces the thumbnails of your images with question marks){% endtrans %}</label>
</div>{% endif %}
{% if config.allow_no_country and config.country_flags and not config.force_flag %}<div class="no-country-option">
<label><input title="No country flag" id="no_country" name="no_country" type="checkbox">
{% trans %}Hide country{% endtrans %} <span class="unimportant hint">{% trans %}(this board displays your country when you post if this is unchecked){% endtrans %}</span></label>
{% trans %}Hide country{% endtrans %}
<br/>
<span class="unimportant hint">{% trans %}(this board displays your country when you post if this is unchecked){% endtrans %}</span></label>
</div>{% endif %}
{% if mod %}
@ -225,7 +239,7 @@
</p>
</td>
</tr>
</table>
</tbody></table>
</form>
</div>
</div>

37
translation.php Normal file
View File

@ -0,0 +1,37 @@
<?php
include "inc/functions.php";
$body = <<<EOT
<DIv class="ban">
<p>Thank you for your interest in contributing a translation to infinity. This page will teach you how.</p>
<p><em>Historical note: infinity is based on a project called vichan (pronunced 6chan) which is in turn based on an older, abandoned project called Tinyboard. Vichan uses a service called Transifex to translate their files. In earlier versions of infinity, I decided to just keep using the vichan files because the only substantial source of new strings was the homepage and the Board configuration page, neither of which were displayed to board users so the existing set of translations worked. However, as time went on and more scripts and features were contributed, strings became out of sync. I originally intended to create 8chan a Transifex account, <s>but Transifex charges for something as simple as bulk imports</s>, so we will use this slightly more complicated process instead. Further, despite how much their charismatic CEO tried to sugarcoat it, the Transifex company <a href="https://github.com/transifex/transifex/issues/206#issuecomment-15243207">abandoned their open source repository</a> and became proprietary software, and then <s>immediately put limits on imports/exports</s>. <a href="https://www.gnu.org/philosophy/who-does-that-server-really-serve.html">Please see this page from the Free Software Foundation for more about the philosophy behind this and the dangers of trusting SaaS with your data. Who does that server really serve?</a></em></p>
<p>Some of my criticism of Transifex was not accurate, I apologize. You are free to either use Transifex or follow the steps below. <a href="https://www.transifex.com/projects/p/infinity/">Here's our Transifex team page</a></p>
<p>infinity uses gettext files for translation. This is what allows us to have boards in many languages on the same site, such as <a href="/argentina/">/argentina/ in Spanish</a>, <a href="/deutsch/">/deutsch/ in German</a> and <a href="/japan2/">/japan2/ in Japanese</a>. gettext files have the .po file extension. You can edit PO files by hand, but I highly recommend using POEdit. It is very easy to make syntax errors without POEdit or similar software.</p>
<ol>
<li>Install <a href="http://poedit.net/">POEdit</a>. POEdit is free software available for Mac, Linux and Windows.</li>
<li>Find your translation file. Go to <a href="https://github.com/ctrlcctrlv/infinity/">our Github project</a>, and in the files list click "inc", then "locale". You will see a list of languages. It's usually self explanatory which code is for which language, but if you're not sure you can check <a href="https://www.gnu.org/software/gettext/manual/html_node/Usual-Language-Codes.html#Usual-Language-Codes">the GNU project's list of usual language codes</a> and search for your language.</li>
</ol>
<p><strong>If your language is listed and you want to update the translation:</strong></p>
<ol>
<li>Download the .po files. <tt>tinyboard.po</tt> contains the strings generated by PHP, like "Comment", "Name", et cetera. <tt>javascript.po</tt> contains the strings generated by JavaScript, like all the fields under [Options]. You can translate one or both. For example, <a href="https://github.com/ctrlcctrlv/infinity/blob/master/inc/locale/fr_FR/LC_MESSAGES/tinyboard.po">here is the French translation of tinyboard.po</a>. To download it, click "Raw" and then save the file to your computer.</li>
<li>Click "Edit a translation" in POEdit. Navigate to the file you downloaded, fill in the translation boxes and save your file.</li>
</ol>
<p><strong>If your language is not listed and you want to add a translation for it:</strong></p>
<ol>
<li>Download the en <em>English</em> .po file. (Tip: If, for example, you want to create a new Spanish dialect translation when Spanish (Spain) already exists, download es_ES and use that as the template.)</li>
<li>Click "Create a new translation" in POEdit and select the po file you downloaded as the template file.</li>
<li>Select your language from the dropdown when prompted.</li>
</ol>
<p><em>Tip: If you would like to attribute your translation to you, you can change your Name and Email in Preferences.</em></p>
<p><em>Another tip: You might find that a string you want to translate is not in the files. Don't panic, I accidentally forget to put strings in {% trans %} tags and _() gettext function all the time so gettext doesn't catch them. Just email me and tell me where I forgot and I'll add it and update tinyboard.po/javascript.po. Some strings I don't want to add for legal reasons. Those are the ones at the bottom including the copyright notice.</em></p>
<p>Once you are done translating, save your .po file in POEdit and send it to admin@8chan.co, or, if you know how, open a pull request on Github with your translated file. Make sure to put the language you translated to in the subject of your email. Thanks in advance for your contribution!</p>
</div>
EOT;
echo Element("page.html", array("config" => $config, "body" => $body, "title" => "Translation tutorial"));