Создав и поддержав проект с открытым исходным кодом, я хочу сразу решить все возможные проблемы с многоязычной поддержкой как проекта, так и сайта.
Многоязычной поддержкой в различных проектах занимаюсь очень давно, начиная с десктопных программ.
Таким образом, имея представление о возможных потребностях, я начал знакомиться с предлагаемыми решениями.
Да, почти все SaaS-сервисы предлагают бесплатное использование проектов с открытым исходным кодом, но в основном все там ориентировано на перевод строковых ресурсов.
А как насчет сайта и документации? К сожалению, я так и не нашел ничего подходящего и начал реализовывать сам.
Скажу сразу, результатом доволен и системой пользуюсь уже почти полгода, хотя предупреждаю, что это не массовое, комплексное решение, а скорее конкретная реализация под мои нужды, но я надеюсь, что некоторые идеи могут быть полезны другим разработчикам.
Для начала перечислю требования, которые я поставил к будущему детищу.
- Вам необходимо локализовать как ресурсы проекта, хранящиеся в формате JSON в формате .
js, так и все тексты и документацию на сайте.
- Ресурс не может быть переведен на другие языки.
То есть, например, я могу накопить тексты на русском языке, а затем отдать их переводчику, и в русской версии сайта эти тексты уже будут доступны.
- На сайте должна быть удобная система, чтобы пользователь мог переводить не переведенные на его язык ресурсы, создавать новый ресурс (текст) или проверять и редактировать существующие тексты на родном языке.
Это должно выглядеть примерно так — пользователь выбирает действие (перевод, проверка), родной язык (а в случае перевода ещё и язык оригинала), а также желаемый объём.
По этим параметрам осуществляется поиск ресурса и предложение пользователю для перевода или редактирования.
Естественно, должен вестись журнал действий пользователя и накапливаться статистика по выполненным работам.
- На сайте должен быть выбор языков, но на каждой странице должны отображаться только те языки, для которых уже есть перевод этой страницы.
- Одна и та же строка может использоваться в нескольких местах.
Например, строка используется в .
js и документации.
То есть ресурс должен быть в одном экземпляре и при его изменении он должен меняться как в JSON, так и в документации.
- В идеале должна быть какая-то система автомодерации, но пока можно ограничиться личным принятием решений при публикации.
На самом деле четырех столов достаточно.
Структура таблицы
Таблица языков с тремя полями имени, родной, iso639. Пример записи: Русский, Русский, ru Таблица текстовых идентификаторов длинных ресурсов, где также можно указать комментарий и тип.CREATE TABLE IF NOT EXISTS `languages` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `_uptime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `_owner` smallint(5) unsigned NOT NULL, `name` varchar(32) NOT NULL, `native` varchar(32) NOT NULL, `iso639` varchar(2) NOT NULL, PRIMARY KEY (`id`), KEY `_uptime` (`_uptime`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8 ; CREATE TABLE IF NOT EXISTS `langid` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `_uptime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `_owner` smallint(5) unsigned NOT NULL, `name` varchar(96) NOT NULL, `comment` text NOT NULL, `restype` tinyint(3) unsigned NOT NULL, `attrib` tinyint(3) unsigned NOT NULL, PRIMARY KEY (`id`), KEY `_uptime` (`_uptime`), KEY `name` (`name`), KEY `restype` (`restype`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8 ; CREATE TABLE IF NOT EXISTS `langlog` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `_uptime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `_owner` smallint(5) unsigned NOT NULL, `iduser` int(10) unsigned NOT NULL, `idlangres` int(10) unsigned NOT NULL, `action` tinyint(3) unsigned NOT NULL, PRIMARY KEY (`id`), KEY `_uptime` (`_uptime`), KEY `iduser` (`iduser`,`idlangres`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8 ; CREATE TABLE IF NOT EXISTS `langres` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `_uptime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `_owner` smallint(5) unsigned NOT NULL, `langid` smallint(5) unsigned NOT NULL, `lang` tinyint(3) unsigned NOT NULL, `text` text NOT NULL, `prev` mediumint(9) unsigned NOT NULL, `verified` tinyint(3) NOT NULL, `size` mediumint(9) unsigned NOT NULL, PRIMARY KEY (`id`), KEY `_uptime` (`_uptime`), KEY `langid` (`langid`,`lang`), KEY `size` (`size`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8 ;
Все ресурсы я разделил на несколько типов: строка JSON, страница сайта, обычный текст, текст в формате MarkDown. Вы, конечно, можете использовать свои собственные типы.
Пример: сancelbtn, Текст для кнопки «Отмена», JSON Таблица текстовых ресурсов langres ( langid, язык, текст, предыдущая).
Мы храним ссылки на идентификатор, язык и сам текст. Последнее поле prev обеспечивает версионирование текста при редактировании и указывает на предыдущую версию ресурса.
Все изменения записываются в таблицу журнала langlog (iduser, idlangres, action).
В поле действия будет указано предпринятое действие – создание, редактирование, проверка.
Не буду останавливаться на работе с пользователями, скажу лишь, что пользователь регистрируется автоматически при отправке перевода или исправления.
Поскольку электронная почта не требуется, пользователю сразу предоставляется логин и пароль.
Все внесенные им изменения будут связаны с его учетной записью.
В дальнейшем он может указать свой адрес электронной почты и другие данные или просто забыть об этой регистрации.
Я нарисовал схему, чтобы вы могли лучше представить все связи между таблицами.
Так как мне нужна возможность вставлять ресурсы в другие ресурсы, я добавил макросы вида #идентификатор#.
Например, в простейшем случае, если у нас есть имя ресурса = «Имя», то мы можем использовать его в ресурсе entername = «Укажите свое #имя#», которое будет заменено на Введите ваше имя .
Теперь для генерации страниц сайта достаточно перебрать все языки и ресурсы соответствующего типа, обработать каждый текст специальной функцией замены и записать результат в отдельную таблицу с готовыми страницами.
Причём обработка происходит таким образом, что если #идентификатор# не найден в текущем языке, то он ищется на других языках.
Вот набросок рекурсивной функции (с защитой от циклов), которая выполняет эту обработку.
Пример функции подстановки PHP public function proceed( $input, $recurse = false )
{
global $db, $syslang;
if ( !$recurse )
$this->chain = array();
$result = '';
$off = 0;
$start = 0;
$len = strlen( $input );
while ( ($off = strpos( $input, '#', $off )) !== false && $off < $len - 2 )
{
$end = strpos( $input, '#', $off + 2 );
if ( $end === false )
break;
if ( $end - $off > $this->lenlimit )
{
$off = $end - 1;
continue;
}
$name = substr( $input, $off + 1, $end - $off - 1 );
$langid = $db->getone("select id from langid where name=Эs", $name );
if ( $langid && !in_array( $langid, $this->chain ))
{
$langres = $db->getrow("select _uptime, id,text from langres where langid=Эs && verified>0
order by if( lang=Эs, 0, 1 ),lang", $langid, $this->lang );
if ( $langres )
{
if ( $langres['_uptime'] > $this->time )
$this->time = $langres['_uptime'];
$result .
= substr( $input, $start, $off - $start ); $off = $end + 1; $start = $off; array_push( $this->chain, $langid ); $result .
= $this->proceed( $langres['text'], true ); array_pop( $this->chain ); if ( $off >= $len - 2 ) break; continue; } } $off = $end - 1; } if ( $start < $len ) $result .
= substr( $input, $start );
return $result;
}
Помимо замены макросов типа #name#, я также немедленно конвертирую разметку MarkDown в HTML и обрабатываю свои собственные директивы.
Например, у меня есть таблица картинок, где одна запись может содержать скриншоты для разных языков, и если я укажу в тексте тег [img "/file/#*indexes#"], то изображение с именем индексируется с Мне нужен замененный язык.
Но самое главное, что я могу генерировать загрузки для различных целей в любом формате.
В качестве примера приведу код генерации файлов JSON; однако за ненадобностью функция подстановки идентификатора не используется.
Генерация JSON-файлов для RU и EN function jsonerror( $message )
{
print $message;
exit();
}
function save_json( $filename )
{
global $db, $original;
preg_match("/^\w*_(?<lang>\w*)\.
js$/", $filename, $matches ); if ( empty( $matches['lang'] )) jsonerror( 'No locale' ); $lang = $db->getrow("select * from languages where iso639=Эs", $matches['lang'] ); if ( !$lang ) jsonerror( 'Unknown locale '.
$matches['lang'] ); $list = $db->getall("select lng.name, r.text from langid as lng left join langres as r on r.langid = lng.id where lng.restype=5 && verified>0 && r.lang=Эs order by lng.name", $lang['id'] ); $out = array(); foreach ( $list as $il ) $out[ $il['name']] = $il['text']; if ( $lang['id'] == 1 ) $original = $out; else foreach ( $original as $ik => $io ) if ( !isset( $out[ $ik ] )) $out[ $ik ] = $io; $output = "/* This file is automatically generated on eonza.org. Use http://www.eonza.org/translate.html to edit or translate these text resources. */ var lng = { \tcode: '$lang[iso639]', \tnative: '$lang[native]', "; foreach ( $out as $ok => $ov ) { if ( strpos( $ov, "'" ) === false ) $text = "'$ov'"; elseif (strpos( $ov, '"' ) === false ) $text = "\"$ov\""; else jsonerror( 'Wrong text:'.
$text ); $output .
= "\t$ok: $text,\r\n"; } $output .
= "\r\n};\r\n"; $jsfile = dirname(__FILE__).
"/i18n/$lang[iso639].
js"; if ( file_exists( $jsfile )) $output .
= file_get_contents( $jsfile ); if (file_put_contents( HOME."tmp/$filename", $output )) print "Save: ".
HOME."tmp/$filename<br>"; else jsonerror( 'Save error:'.
HOME."tmp/$filename" );
}
$original = array();
$files = array( 'en', 'ru');
foreach ( $files as $if )
save_json( "locale_$if.js" );
$zip = new ZipArchive();
print $zip->open( HOME."tmp/locale.zip", ZipArchive::CREATE );
foreach ( $files as $f )
print $zip->addFile( HOME."tmp/locale_$f.js", "locale_$f.js" );
print $zip->close();
print "Finish<br><a href='/tmp/locale.zip'>ZIP file</a>";
Таким образом, не затрачивая особых усилий, я реализовал почти все, что хотел.
Нереализованными остались только те вещи, которые не актуальны на данный момент из-за низкой активности на сайте.
Но были добавлены дополнительные функции, которые были необходимы в процессе использования.
Например, получение текстового файла с ресурсами, требующими перевода, и загрузка обратно переведенного текста.
Желающие могут посмотрите на рабочую страницу , где пользователи могут переводить, редактировать и создавать новые ресурсы для моего проекта.
Теги: #локализация сайта #мультиязычная #JSON-локализация #занимаюсь пиаром
-
Что В Цвете?
19 Oct, 24 -
Игры-Одевалки: «Сделай Или Сломай»
19 Oct, 24 -
День Инвестора Addventure
19 Oct, 24 -
Чего Вы Хотите От Работодателя?
19 Oct, 24 -
Я Видел Будущее. Глава 1
19 Oct, 24 -
Ну, Ты Не Ждал?
19 Oct, 24 -
Как Две Недели?!
19 Oct, 24