Переводы

This commit is contained in:
n1rwana 2023-07-25 23:00:37 +03:00
parent a2384cc231
commit 25e5da1e74
10 changed files with 488 additions and 3 deletions

View file

@ -550,4 +550,227 @@ final class AdminPresenter extends OpenVKPresenter
$this->redirect("/admin/users/id" . $user->getId()); $this->redirect("/admin/users/id" . $user->getId());
} }
function renderTranslation(): void
{
$lang = $this->queryParam("lang") ?? "ru";
$q = $this->queryParam("q");
$lines = [];
$new_key = true;
if ($lang === "any" || $this->queryParam("langs")) {
if (!$q || trim($q) === "") {
$this->flashFail("err", tr("translation_enter_query_first"));
return;
}
$locales = $this->queryParam("langs") ? explode(",", $this->queryParam("langs")) : array_filter(scandir(__DIR__ . "/../../locales/"), function ($file) {
return preg_match('/\.strings$/', $file);
});
$_locales = [];
foreach ($locales as $locale)
$_locales[] = explode(".", $locale)[0];
foreach ($locales as $locale) {
$handle = fopen(__DIR__ . "/../../locales/$locale" . ($this->queryParam("langs") ? ".strings" : ''), "r");
if ($handle) {
$i = 0;
while (($line = fgets($handle)) !== false) {
$i++;
if (preg_match('/"(.*)" = "(.*)"(;)?/', $line, $matches)) {
$val = ["index" => $i, "key" => $matches[1], "lang" => explode(".", $locale)[0], "value" => $matches[2]];
if (!in_array($val["key"], ["__locale", "__WinEncoding", "__transNames"])) {
if ($q) {
if (str_contains($q, "key:")) {
continue;
} else if (str_contains($q, "value:")) {
$_exact_value_match = preg_match('/value:(.*)/', $q, $_value_matches);
if ($_exact_value_match && $_value_matches[1] !== $val["value"]) {
continue;
}
} else {
if (!str_contains(mb_strtolower($line), mb_strtolower($q))) {
continue;
}
}
}
$lines[] = $val;
}
}
}
fclose($handle);
$new_key = false;
} else {
$this->flash("err", tr("translation_locale_file_not_found"));
}
}
if (str_contains($q, "key:")) {
$_exact_key_match = preg_match('/key:(.*)/', $q, $_key_matches);
if ($_exact_key_match && $_key_matches[1]) {
$i = 0;
$used_langs = [];
foreach ($_locales as $locale) {
if ($i === sizeof($_locales)) break;
$handle = fopen(__DIR__ . "/../../locales/$locale.strings", "r");
$value = "";
if ($handle) {
while (($line = fgets($handle)) !== false) {
if (preg_match('/"(' . $_key_matches[1] . ')" = "(.*)"(;)?/', $line, $matches)) {
$value = $matches[2];
}
$new_key = isset($value);
}
fclose($handle);
}
if (!in_array($locale, $used_langs)) {
$lines[] = ["index" => $i, "key" => $_key_matches[1], "lang" => $locale, "value" => $value];
}
$used_langs[] = $locale;
$i++;
}
}
}
} else {
$new_key = false;
$handle = fopen(__DIR__ . "/../../locales/$lang.strings", "r");
if ($handle) {
$i = 0;
while (($line = fgets($handle)) !== false) {
$i++;
if (preg_match('/"(.*)" = "(.*)"(;)?/', $line, $matches)) {
$val = ["index" => $i, "key" => $matches[1], "lang" => $lang, "value" => $matches[2]];
if (!in_array($val["key"], ["__locale", "__WinEncoding", "__transNames"])) {
if ($q) {
if (str_contains($q, "key:")) {
$_exact_key_match = preg_match('/key:(.*)/', $q, $_key_matches);
if ($_exact_key_match && $_key_matches[1] !== $val["key"]) {
continue;
}
} else if (str_contains($q, "value:")) {
$_exact_value_match = preg_match('/value:(.*)/', $q, $_value_matches);
if ($_exact_value_match && $_value_matches[1] !== $val["value"]) {
continue;
}
} else {
if (!str_contains(mb_strtolower($line), mb_strtolower($q))) {
continue;
}
}
}
$lines[] = $val;
}
}
}
fclose($handle);
} else {
$this->flash("err", tr("translation_locale_file_not_found"));
}
}
$this->template->languages = getLanguages();
$this->template->activeLang = $lang;
$this->template->keys = $lines;
$this->template->q = str_replace('"', '', $q);
$this->template->scrollTo = $this->queryParam("s");
$this->template->langs = $this->queryParam("langs");
$this->template->new_key = $new_key;
}
function renderTranslateKey(): void
{
if ($_SERVER["REQUEST_METHOD"] === "POST") {
$this->assertNoCSRF();
if (empty($this->postParam("strings"))) {
$lang = $this->postParam("lang");
$key = $this->postParam("key");
$value = addslashes($this->postParam("value"));
$handle = fopen(__DIR__ . "/../../locales/$lang.strings", "c");
if ($handle) {
if ($this->postParam("act") !== "delete") {
$file = file_get_contents(__DIR__ . "/../../locales/$lang.strings");
if ($file) {
$handle = fopen(__DIR__ . "/../../locales/$lang.strings", "c");
if (preg_match('/"(' . $key . ')" = "(.*)";/', $file)) {
$replacement = rtrim(preg_replace('/"(' . $key . ')" = "(.*)";/', '"$1" = "' . $value . '";', $file), "");
if (file_put_contents(__DIR__ . "/../../locales/$lang.strings", $replacement)) {
fclose($handle);
$this->returnJson(["success" => true]);
} else {
fclose($handle);
$this->returnJson(["success" => false, "error" => tr("translation_file_writing_error")]);
}
} else {
$file .= "\"$key\" = \"$value\";\n";
if (fwrite($handle, $file)) {
fclose($handle);
$this->returnJson(["success" => true]);
} else {
fclose($handle);
$this->returnJson(["success" => false, "error" => tr("translation_file_writing_error")]);
}
}
} else {
$this->returnJson(["success" => false, "error" => tr("translation_locale_file_not_found")]);
}
} else {
$file = file(__DIR__ . "/../../locales/$lang.strings");
$new_file = [];
foreach ($file as &$line) {
if (!preg_match('/"(' . $key . ')" = "(' . $value . ')";/', $line)) {
$new_file[] = $line;
}
}
file_put_contents(__DIR__ . "/../../locales/$lang.strings", implode("", $new_file));
fclose($handle);
$this->returnJson(["success" => true]);
}
} else {
$this->returnJson(["success" => false, "error" => tr("translation_file_reading_error")]);
}
} else {
$objects = explode(";", $this->postParam("strings"));
if (sizeof($objects) < 2) {
$this->returnJson(["success" => false, "error" => tr("translation_enter_at_least_two_values")]);
}
$succ = 0;
foreach ($objects as $object) {
$data = explode(":", $object);
$lang = $data[0];
$key = $data[1];
$value = addslashes($data[2]);
$file = file_get_contents(__DIR__ . "/../../locales/$lang.strings");
if ($file) {
$handle = fopen(__DIR__ . "/../../locales/$lang.strings", "c");
if ($handle) {
if (preg_match('/"(' . $key . ')" = "(.*)";/', $file)) {
$replacement = preg_replace('/"(' . $key . ')" = "(.*)";/', '"$1" = "' . $value . '";', $file);
if (file_put_contents(__DIR__ . "/../../locales/$lang.strings", $replacement)) {
$succ++;
}
} else {
$file .= "\"$key\" = \"$value\";\n";
if (fwrite($handle, $file)) {
$succ++;
}
}
fclose($handle);
}
}
}
$this->returnJson(["success" => true, "count" => $succ]);
}
} else {
$this->notFound();
}
}
} }

View file

@ -204,6 +204,7 @@
{var $menuLinksAvaiable = sizeof(OPENVK_ROOT_CONF['openvk']['preferences']['menu']['links']) > 0 && $thisUser->getLeftMenuItemStatus('links')} {var $menuLinksAvaiable = sizeof(OPENVK_ROOT_CONF['openvk']['preferences']['menu']['links']) > 0 && $thisUser->getLeftMenuItemStatus('links')}
<div n:if="$canAccessAdminPanel || $canAccessHelpdesk || $menuLinksAvaiable" class="menu_divider"></div> <div n:if="$canAccessAdminPanel || $canAccessHelpdesk || $menuLinksAvaiable" class="menu_divider"></div>
<a href="/admin" class="link" n:if="$canAccessAdminPanel" title="{_admin} [Alt+Shift+A]" accesskey="a">{_admin}</a> <a href="/admin" class="link" n:if="$canAccessAdminPanel" title="{_admin} [Alt+Shift+A]" accesskey="a">{_admin}</a>
<a href="/translation.php" class="link" n:if="$canAccessAdminPanel" title="{_translations}">{_translations}</a>
<a href="/support/tickets" class="link" n:if="$canAccessHelpdesk">{_helpdesk} <a href="/support/tickets" class="link" n:if="$canAccessHelpdesk">{_helpdesk}
{if $helpdeskTicketNotAnsweredCount > 0} {if $helpdeskTicketNotAnsweredCount > 0}
(<b>{$helpdeskTicketNotAnsweredCount}</b>) (<b>{$helpdeskTicketNotAnsweredCount}</b>)

View file

@ -124,6 +124,9 @@
<li> <li>
<a href="/admin/settings/tuning">{_admin_settings_tuning}</a> <a href="/admin/settings/tuning">{_admin_settings_tuning}</a>
</li> </li>
<li>
<a href="/admin/translation">{_translations}</a>
</li>
<li> <li>
<a href="/admin/settings/appearance">{_admin_settings_appearance}</a> <a href="/admin/settings/appearance">{_admin_settings_appearance}</a>
</li> </li>

View file

@ -0,0 +1,212 @@
{extends "@layout.xml"}
{block title}
{_translations}
{/block}
{block heading}
{include title}
{/block}
{block content}
<form class="aui">
<div class="field-group">
<label>{_language}</label>
<select n:attr="style => $langs ? 'display: none;' : ''" id="lang-select" name="lang" class="select" onchange="onLanguageSelectChanged(this)">
<option value="any" n:attr="selected => $activeLang === 'any'">{_s_any|firstUpper}</option>
<option value="comma-separated">{_translation_comma_separated}</option>
<option
n:foreach="$languages as $language"
n:attr="selected => $activeLang === $language['code']"
value="{$language['code']}"
>[{$language["code"]}] {$language["native_name"]}</option>
</select>
<form class="aui" style="display: flex;">
<input value="{$langs}" n:attr="style => $langs ? '' : 'display: none;'" id="langs-comma-separated" autocomplete="off" class="text long-field" type="text" placeholder="{_translation_comma_separated_langs_placeholder}" name="langs"/>
<input n:attr="value => $q" id="quickSearchInput" autocomplete="off" class="text long-field" type="text" placeholder="{_translation_search}" name="q" accesskey="Q"/>
<button class="aui-button aui-button-primary" type="submit">
<span class="aui-icon aui-icon-small aui-iconfont-search">{_header_search}</span>
</button>
<a n:attr="style => $langs ? '' : 'display: none;'" id="back-to-lang-select-btn" class="aui-button aui-button-primary" onclick="backToLangSelect(this)">
<span class="aui-icon aui-icon-small aui-iconfont-arrow-left">{_select_language}</span>
</a>
</form>
</div>
</form>
<hr/>
<form class="aui" style="margin: 0;" method="post" onsubmit="onKeyValueFormSubmit(this, 'new')">
<div class="field-group">
<select id="lang-new" name="lang" class="select">
<option
n:foreach="$languages as $language"
n:attr="selected => $activeLang === $language['code']"
value="{$language['code']}"
>[{$language["code"]}] {$language["native_name"]}</option>
</select>
<input class="text long-field" type="text" id="key-new" name="key" value="" placeholder="{_translation_key}"/>
<input class="text long-field" type="text" id="value-new" name="value" value="" placeholder="{_translation_value}"/>
<button class="aui-button aui-button-primary" type="submit">
<span class="aui-icon aui-icon-small aui-iconfont-add">{_save}</span>
</button>
</div>
</form>
<hr/>
<form n:attr="id => $new_key ? ('key-l-' . $key['lang']) : ''" class="aui" n:foreach="$keys as $key" style="margin: 0;" method="post" onsubmit="onKeyValueFormSubmit(this, {$key['index']})">
<div class="field-group" style="display: flex; gap: 8px;">
<div style="width: 3em;">
<img src="/assets/packages/static/openvk/img/flags/{$key['lang']}.gif" alt="{$key['lang']}" style="margin: 25% 0 0 50%;"/>
</div>
<input n:attr="lang => $new_key ? $key['lang'] : ''" class="text long-field {=$new_key ? 'key-l-name' : ''}" type="text" id="key-{$key['index']}" name="key" value="{$key['key']}" placeholder="{_translation_key}"/>
<input n:attr="lang => $new_key ? $key['lang'] : ''" class="text long-field {=$new_key ? 'key-l-value' : ''}" type="text" id="value-{$key['index']}" name="value" value="{$key['value']}" placeholder="{_translation_value}"/>
<input type="hidden" id="lang-{$key['index']}" value="{$key['lang']}" />
<button class="aui-button aui-button-primary" type="submit">
<span class="aui-icon aui-icon-small aui-iconfont-export">{_save}</span>
</button>
<a class="aui-button aui-button-primary" onclick="copyKey({$key['index']})">
<span class="aui-icon aui-icon-small aui-iconfont-copy">{_copy}</span>
</a>
<a class="aui-button aui-button-primary" onclick="openKeyInNewTab({$key['key']})">
<span class="aui-icon aui-icon-small aui-iconfont-world">{_translate}</span>
</a>
<a class="aui-button aui-button-primary" onclick="deleteKey({$key['index']})">
<span class="aui-icon aui-icon-small aui-iconfont-delete">{_delete}</span>
</a>
</div>
</form>
<center n:if="sizeof($keys) === 0" style="font-size: 24px;">
<div>{_translation_nothing_found}. :(</div>
<a onclick="openKeyInNewTab({str_replace('key:', '', $q)})">{tr("translation_start_translate_key", $q)}</a>
</center>
<button class="aui-button aui-button-primary" n:if="$new_key" onclick="saveAllKeys()">{_save_all}</button>
<script>
function onLanguageSelectChanged(e) {
if (e.value !== 'comma-separated') {
let q = {=urlencode($q)};
window.location.href = "/admin/translation?lang=" + e.value + (q ? ("&q=" + q) : "");
} else {
document.getElementById('back-to-lang-select-btn').style.display = '';
document.getElementById('langs-comma-separated').style.display = '';
document.getElementById('lang-select').style.display = 'none';
}
}
function onKeyValueFormSubmit(e, index) {
if (event.preventDefault) {
event.preventDefault();
}
const key = document.getElementById('key-' + index).value;
const value = document.getElementById('value-' + index).value;
const lang = document.getElementById('lang-' + index).value;
$.ajax({
type: "POST",
url: "/admin/translate",
data: {
lang: lang,
key: key,
value: value,
hash: {$csrfToken}
},
success: (response) => {
if (response.success) {
window.location.href = window.location.pathname + window.location.search + {$scrollTo ? '"&s=" + window.scrollY' : ''};
} else {
alert(tr("error") + ": " + response.error);
}
}
})
}
function copyKey(index) {
const key = document.getElementById('key-' + index).value;
const value = document.getElementById('value-' + index).value;
document.getElementById('key-new').value = key;
document.getElementById('value-new').value = value;
}
function deleteKey(index) {
const key = document.getElementById('key-' + index).value;
const value = document.getElementById('value-' + index).value;
const lang = document.getElementById('lang-' + index).value;
$.ajax({
type: "POST",
url: "/admin/translate",
data: {
act: "delete",
lang: lang,
key: key,
value: value,
hash: {$csrfToken}
},
success: (response) => {
if (response.success) {
window.location.href = window.location.pathname + window.location.search + {$scrollTo ? '"&s=" + window.scrollY' : ''};
} else {
alert(tr("error") + ": " + response.error);
}
}
})
}
function backToLangSelect(e) {
if (e.preventDefault) {
e.preventDefault();
}
document.getElementById('back-to-lang-select-btn').style.display = 'none';
document.getElementById('langs-comma-separated').style.display = 'none';
const langSelect = document.getElementById('lang-select');
langSelect.value = 'ru';
langSelect.style.display = '';
}
window.scrollTo(window.scrollX, {=$scrollTo});
function saveAllKeys() {
if (!{=$new_key}) return false;
let keys = [];
let values = [];
let objects = [];
let _names = $(".key-l-name").map(function() {
keys[$(this).attr('lang')] = this.value;
}).get();
let _values = $(".key-l-value").map(function() {
values[$(this).attr('lang')] = this.value;
}).get();
let _languages = Object.keys(keys);
_languages.map((language) => {
if (language && keys[language] && values[language])
objects.push(`${ language}:${ keys[language]}:${ values[language]}`);
});
console.log(objects.join(";"));
$.ajax({
type: "POST",
url: "/admin/translate",
data: {
strings: objects.join(";"),
hash: {$csrfToken}
},
success: (response) => {
if (response.success) {
window.location.reload();
} else {
alert(tr("error") + ": " + response.error);
}
}
})
}
function openKeyInNewTab(keyName) {
window.location.href = '/admin/translation?lang=any&q=key:' + keyName;
}
</script>
{/block}

View file

@ -325,6 +325,12 @@ routes:
handler: "Admin->bannedLink" handler: "Admin->bannedLink"
- url: "/admin/bannedLink/id{num}/unban" - url: "/admin/bannedLink/id{num}/unban"
handler: "Admin->unbanLink" handler: "Admin->unbanLink"
- url: "/admin/translation"
handler: "Admin->translation"
- url: "/translation.php"
handler: "Admin->translation"
- url: "/admin/translate"
handler: "Admin->translateKey"
- url: "/upload/photo/{text}" - url: "/upload/photo/{text}"
handler: "VKAPI->photoUpload" handler: "VKAPI->photoUpload"
- url: "/method/{text}.{text}" - url: "/method/{text}.{text}"

View file

@ -1537,3 +1537,23 @@
"mobile_like" = "Like"; "mobile_like" = "Like";
"mobile_user_info_hide" = "Hide"; "mobile_user_info_hide" = "Hide";
"mobile_user_info_show_details" = "Show details"; "mobile_user_info_show_details" = "Show details";
"translation_enter_query_first" = "First enter the query";
"translation_locale_file_not_found" = "Locale file not found";
"translation_file_writing_error" = "Error when writing to a file";
"translation_file_reading_error" = "Error when reading a file";
"translation_enter_at_least_two_values" = "Enter values for at least two locales";
"translations" = "Translations";
"translation_you_are_creating_a_new_key" = "You are creating a new key";
"translation_you_are_creating_a_new_key_description" = "Set a value for at least two languages and click \"Save All\"";
"language" = "Language";
"translation_comma_separated" = "Comma-separated";
"translation_comma_separated_langs_placeholder" = "Language codes separated by commas (ru,en,...)";
"translation_search" = "Search (key:NAME) to create a key";
"translation_key" = "Key";
"translation_value" = "Value";
"copy" = "Copy";
"translate" = "Translate";
"translation_nothing_found" = "Nothing was found";
"translation_start_translate_key" = "Start key $1 translation";
"save_all" = "Save all";

View file

@ -673,4 +673,4 @@
"edit_action" = "Ŝanĝi"; "edit_action" = "Ŝanĝi";
"warning" = "Averto"; "warning" = "Averto";
"question_confirm" = "Ĉi tiu ago ne povas esti malfarita. Ĉu vi vere volas fari ĝin?"; "question_confirm" = "Ĉi tiu ago ne povas esti malfarita. Ĉu vi vere volas fari ĝin?";

View file

@ -1430,3 +1430,23 @@
"mobile_like" = "Нравится"; "mobile_like" = "Нравится";
"mobile_user_info_hide" = "Скрыть"; "mobile_user_info_hide" = "Скрыть";
"mobile_user_info_show_details" = "Показать подробнее"; "mobile_user_info_show_details" = "Показать подробнее";
"translation_enter_query_first" = "Сначала введите запрос";
"translation_locale_file_not_found" = "Файл с локалью не найден";
"translation_file_writing_error" = "Ошибка при записи в файл";
"translation_file_reading_error" = "Ошибка при чтении файла";
"translation_enter_at_least_two_values" = "Введите значения хотя бы для двух локалей";
"translations" = "Переводы";
"translation_you_are_creating_a_new_key" = "Вы создаете новый ключ";
"translation_you_are_creating_a_new_key_description" = "Задайте значение как минимум для двух языков и нажмите \"Сохранить все\"";
"language" = "Язык";
"translation_comma_separated" = "Через запятую";
"translation_comma_separated_langs_placeholder" = "Коды языков через запятую (ru,en,...)";
"translation_search" = "Поиск (key:ИМЯ чтобы создать ключ)";
"translation_key" = "Ключ";
"translation_value" = "Значение";
"copy" = "Скопировать";
"translate" = "Перевести";
"translation_nothing_found" = "Ничего не нашлось";
"translation_start_translate_key" = "Начать перевод ключа $1";
"save_all" = "Сохранить все";

View file

@ -773,4 +773,4 @@
"reset" = "Сбросъ"; "reset" = "Сбросъ";
"closed_group_post" = "Это высказыванiе изъ закрытого общѣства"; "closed_group_post" = "Это высказыванiе изъ закрытого общѣства";
"deleted_target_comment" = "Этотъ отзыв принадлѣжит к удалѣнному высказыванiю"; "deleted_target_comment" = "Этотъ отзыв принадлѣжит к удалѣнному высказыванiю";

View file

@ -978,4 +978,4 @@
"reset" = "Сброс"; "reset" = "Сброс";
"closed_group_post" = "Эта запись из закрытого собрания"; "closed_group_post" = "Эта запись из закрытого собрания";
"deleted_target_comment" = "Этот отзыв надлежит к удалённой записи"; "deleted_target_comment" = "Этот отзыв надлежит к удалённой записи";