Как Я Реализовал Мультиязычность На Сайте И В Проекте

Создав и поддержав проект с открытым исходным кодом, я хочу сразу решить все возможные проблемы с многоязычной поддержкой как проекта, так и сайта.

Многоязычной поддержкой в различных проектах занимаюсь очень давно, начиная с десктопных программ.

Таким образом, имея представление о возможных потребностях, я начал знакомиться с предлагаемыми решениями.

Да, почти все SaaS-сервисы предлагают бесплатное использование проектов с открытым исходным кодом, но в основном все там ориентировано на перевод строковых ресурсов.

А как насчет сайта и документации? К сожалению, я так и не нашел ничего подходящего и начал реализовывать сам.

Скажу сразу, результатом доволен и системой пользуюсь уже почти полгода, хотя предупреждаю, что это не массовое, комплексное решение, а скорее конкретная реализация под мои нужды, но я надеюсь, что некоторые идеи могут быть полезны другим разработчикам.

Для начала перечислю требования, которые я поставил к будущему детищу.

  1. Вам необходимо локализовать как ресурсы проекта, хранящиеся в формате JSON в формате .

    js, так и все тексты и документацию на сайте.

  2. Ресурс не может быть переведен на другие языки.

    То есть, например, я могу накопить тексты на русском языке, а затем отдать их переводчику, и в русской версии сайта эти тексты уже будут доступны.

  3. На сайте должна быть удобная система, чтобы пользователь мог переводить не переведенные на его язык ресурсы, создавать новый ресурс (текст) или проверять и редактировать существующие тексты на родном языке.

    Это должно выглядеть примерно так — пользователь выбирает действие (перевод, проверка), родной язык (а в случае перевода ещё и язык оригинала), а также желаемый объём.

    По этим параметрам осуществляется поиск ресурса и предложение пользователю для перевода или редактирования.

    Естественно, должен вестись журнал действий пользователя и накапливаться статистика по выполненным работам.

  4. На сайте должен быть выбор языков, но на каждой странице должны отображаться только те языки, для которых уже есть перевод этой страницы.

  5. Одна и та же строка может использоваться в нескольких местах.

    Например, строка используется в .

    js и документации.

    То есть ресурс должен быть в одном экземпляре и при его изменении он должен меняться как в JSON, так и в документации.

  6. В идеале должна быть какая-то система автомодерации, но пока можно ограничиться личным принятием решений при публикации.

Отображение изменений в реальном времени для меня было не актуально, и я решил сделать несколько промежуточных таблиц со всей внутренней кухней, а затем по команде собирать JSON и генерировать страницы самого сайта.

На самом деле четырех столов достаточно.

Структура таблицы

  
  
   

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 ;

Таблица языков с тремя полями имени, родной, iso639. Пример записи: Русский, Русский, ru Таблица текстовых идентификаторов длинных ресурсов, где также можно указать комментарий и тип.

Все ресурсы я разделил на несколько типов: строка 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-локализация #занимаюсь пиаром

Вместе с данным постом часто просматривают: