Лучший веб-сайт с несколькими языками

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

Позвольте мне сначала набросать ситуацию, которую я ищу

Я собираюсь обновить / переработать систему управления контентом, которую я использую довольно долгое время. Тем не менее, я чувствую, что многоязычность – это большое улучшение этой системы. Раньше я не использовал никаких фреймворков, но я собираюсь использовать Laraval4 для предстоящего проекта. Laravel кажется лучшим выбором более чистого способа программирования PHP. Sidenote: Laraval4 should be no factor in your answer . Я ищу общие способы перевода, которые независимы от платформы / рамки.

Что нужно перевести

Поскольку система, которую я ищу, должна быть максимально удобной для пользователя, метод управления переводом должен находиться внутри CMS. Не должно быть необходимости запуска FTP-соединения для изменения файлов перевода или любых шаблонов, обработанных html / php.

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

Что я придумал сам

Поскольку я уже искал, сам читал и пробовал. Есть несколько вариантов, которые у меня есть. Но я до сих пор не чувствую, что достиг наилучшего метода для того, чего я действительно ищу. Прямо сейчас, это то, что я придумал, но этот метод также имеет побочные эффекты.

  1. PHP Parsed Templates : система шаблонов должна анализироваться PHP. Таким образом, я могу вставить переведенные параметры в HTML, не открывая шаблоны и не изменяя их. Кроме того, PHP-шаблоны дают мне возможность иметь 1 шаблон для всего веб-сайта вместо того, чтобы иметь подпапку для каждого языка (что у меня было раньше). Метод достижения этой цели может быть Smarty, TemplatePower, Blade Laravel или любым другим парсером шаблонов. Как я уже сказал, это должно быть независимым от письменного решения.
  2. База данных : возможно, мне не нужно упоминать об этом еще раз. Но решение должно быть основано на базе данных. CMS нацелен быть объектно-ориентированным и MVC, поэтому мне нужно будет думать о логической структуре данных для строк. Поскольку мои шаблоны будут структурированы: шаблоны / Controller / View.php, возможно, эта структура будет иметь наибольший смысл: Controller.View.parameter . В таблице базы данных эти поля будут длинными с полем value . Внутри шаблонов мы можем использовать некоторый метод сортировки, такой как echo __('Controller.View.welcome', array('name', 'Joshua')) а параметр содержит Welcome, :name . Таким образом, результат – « Welcome, Joshua . Это кажется хорошим способом сделать это, потому что параметры, такие как: имя, легко понять редактором.
  3. Низкая загрузка базы данных : Конечно, вышеупомянутая система будет загружать нагрузку на базу данных, если эти строки будут загружаться в пути. Поэтому мне понадобится система кэширования, которая повторно отображает языковые файлы, как только они будут отредактированы / сохранены в среде администрирования. Поскольку файлы сгенерированы, необходим хороший формат файловой системы. Я думаю, мы можем пойти с languages/en_EN/Controller/View.php или .ini, что вам больше всего подходит. Возможно, даже .ini даже разобрался быстрее. Этот файл должен содержать данные в format parameter=value; , Я думаю, это лучший способ сделать это, так как каждый визуализируемый вид может включать в себя собственный языковой файл, если он существует. Параметры языка затем должны быть загружены в определенное представление, а не в глобальную область, чтобы предотвратить переписывание параметров друг от друга.
  4. Перевод таблицы базы данных : на самом деле это то, о чем я больше всего беспокоюсь. Я ищу способ создания переводов Новости / Страницы / и т. Д. как можно быстрее. Наличие двух таблиц для каждого модуля (например, News и News_translations ) является вариантом, но, похоже, много работы для получения хорошей системы. Одна из вещей, которые я придумал, основана на системе data versioning данных, которую я написал: есть одно имя таблицы базы данных Translations , эта таблица имеет уникальную комбинацию language , tablename и primarykey . Например: en_En / News / 1 (ссылка на английскую версию статьи новостей с ID = 1). Но есть два огромных недостатка в этом методе: в первую очередь эта таблица имеет тенденцию довольно долгое время с большим количеством данных в базе данных, а во-вторых, было бы неплохо использовать эту настройку для поиска в таблице. Например, поиск рекламного пула элемента будет полным текстовым поиском, который довольно тупой. Но, с другой стороны, это быстрый способ создания переводимого контента в каждой таблице очень быстро, но я не верю, что это pro overweights con.
  5. Front-End Work : Кроме того, интерфейсу потребуется некоторое размышление. Разумеется, мы сохранили бы доступные языки в базе данных и (де) активны те, которые нам нужны. Таким образом, сценарий может генерировать выпадающий список, чтобы выбрать язык, а внутренний сервер может автоматически решить, какие переводы можно сделать с помощью CMS. Затем выбранный язык (например, en_EN) будет использоваться при получении языкового файла для представления или для правильного перевода элемента контента на веб-сайте.

Итак, вот они. Мои идеи до сих пор. Они даже не включают параметры локализации для дат и т. Д. Однако, поскольку мой сервер поддерживает PHP5.3.2 +, лучшим вариантом является использование внутреннего расширения, как описано здесь: http://devzone.zend.com/1500/internationalization-in -php-53 / – но это было бы полезно на любом последующем стадионе развития. На данный момент основная проблема заключается в том, как получить лучшую практику перевода контента на веб-сайт.

Помимо всего, что я объяснил здесь, у меня есть еще одна вещь, которую я еще не решил, это выглядит как простой вопрос, но на самом деле это дает мне головные боли:

Перевод URL? Должны ли мы это делать или нет? и каким образом?

Итак .. если у меня есть этот url: http://www.domain.com/about-us а английский – мой язык по умолчанию. Должен ли этот URL-адрес быть переведен на http://www.domain.com/over-ons когда я выбираю голландский язык как мой язык? Или мы должны идти по легкой дороге и просто изменять содержимое страницы, видимое в /about . Последнее не похоже на допустимый вариант, поскольку это приведет к созданию нескольких версий одного и того же URL-адреса, что приведет к неправильному индексированию содержимого.

Другой вариант – вместо этого использовать http://www.domain.com/nl/about-us . Это создает по крайней мере уникальный URL для каждого контента. Также было бы легче перейти на другой язык, например http://www.domain.com/en/about-us а предоставленный URL-адрес легче понять как для посетителей Google, так и для людей. Используя этот параметр, что нам делать с языками по умолчанию? Должен ли язык по умолчанию удалять выбранный по умолчанию язык? Поэтому перенаправление http://www.domain.com/en/about-us на http://www.domain.com/about-us … В моих глазах это лучшее решение, потому что, когда CMS настроена для только на одном языке нет необходимости указывать этот идентификатор в URL-адресе.

И третий вариант – это комбинация из обоих вариантов: использование «language-ident-less–URL» ( http://www.domain.com/about-us ) для основного языка. И используйте URL-адрес с переведенным блоком SEO для подъязыков: http://www.domain.com/nl/over-ons & http://www.domain.com/de/uber-uns

Надеюсь, мой вопрос заставит ваши головы взломать, они наверняка треснули мои! Это помогло мне уже сейчас разобраться в этом вопросе. Дала мне возможность просмотреть методы, которые я использовал раньше, и идея, которую я имею для моей предстоящей CMS.

Я хотел бы поблагодарить вас за то, что вы нашли время, чтобы прочитать эту кучу текста!

// Edit #1 :

Я забыл упомянуть: функция __ () является псевдонимом для перевода заданной строки. Внутри этого метода, очевидно, должен быть какой-то метод возврата, когда текст по умолчанию загружается, когда еще нет доступных переводов. Если перевод отсутствует, он должен быть вставлен или файл перевода должен быть регенерирован.

Посылка темы

На многоязычном сайте есть три различных аспекта:

  • перевод интерфейса
  • содержание
  • маршрутизация URL-адресов

Хотя все они взаимосвязаны по-разному, с точки зрения CMS они управляются с использованием разных элементов пользовательского интерфейса и хранятся по-разному. Вы, кажется, уверены в своей реализации и понимании первых двух. Вопрос касался последнего аспекта – «Перевод URL-адресов? Должны ли мы это делать или нет?» И каким образом?

С каким URL может быть сделан URL?

Очень важно то, что вы не идете с IDN . Вместо этого предпочитают транслитерацию (также: транскрипция и латинизация). Хотя на первый взгляд IDN представляется жизнеспособным вариантом для международных URL-адресов, он фактически не работает так, как рекламируется по двум причинам:

  • некоторые браузеры превратят символы не-ASCII, такие как 'ч' или 'ž' в '%D1%87' и '%C5%BE'
  • если пользователь имеет собственные темы, шрифт темы, скорее всего, не будет содержать символов для этих букв

Я на самом деле пытался использовать IDN несколько лет назад в проекте на основе Yii (ужасная структура, IMHO). Перед тем, как очистить это решение, я столкнулся с обеими вышеупомянутыми проблемами. Кроме того, я подозреваю, что это может быть вектор атаки.

Доступные параметры … как я их вижу.

В принципе у вас есть два варианта, которые можно абстрагировать как:

  • http://site.tld/[:query] : где [:query] определяет выбор языка и контента

  • http://site.tld/[:language]/[:query] : где [:language] часть URL определяет выбор языка, а [:query] используется только для идентификации содержимого

Запрос есть A и Ω ..

Допустим, вы выбрали http://site.tld/[:query] .

В этом случае у вас есть один основной источник языка: содержимое сегмента [:query] ; и два дополнительных источника:

  • значение $_COOKIE['lang'] для этого конкретного браузера
  • список языков в HTTP Accept-Language (1) , (2) заголовок

Во-первых, вам нужно сопоставить запрос с одним из определенных шаблонов маршрутизации (если ваш выбор – Laravel, а затем прочитайте здесь ). При успешном совпадении шаблона вам нужно найти язык.

Вам придется пройти через все сегменты шаблона. Найдите потенциальные переводы для всех этих сегментов и определите, какой язык был использован. Два дополнительных источника (cookie и header) будут использоваться для разрешения конфликтов маршрутизации, когда (а не «если») они возникают.

Возьмем, к примеру: http://site.tld/blog/novinka .

Это транслитерация "блог, новинка" , что на английском означает примерно "blog", "latest" .

Как вы уже заметили, на русском языке «блог» будет транслитерироваться как «блог». Это означает, что для первой части [:query] вы (в лучшем случае ) попадете в список ['en', 'ru'] возможных языков. Затем вы берете следующий сегмент – «новинка». В списке возможностей может быть только один язык: ['ru'] .

Когда в списке есть один элемент, вы успешно нашли этот язык.

Но если в итоге вы получите 2 (пример: русский и украинский) или больше возможностей .. или 0 возможностей, в зависимости от обстоятельств. Вам нужно будет использовать cookie и / или заголовок, чтобы найти правильный вариант.

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

Язык как параметр

Альтернативой является использование URL-адреса, который может быть определен как http://site.tld/[:language]/[:query] . В этом случае при переводе запроса вам не нужно угадывать язык, потому что в тот момент вы уже знаете, что использовать.

Существует также вторичный источник языка: значение cookie. Но здесь нет смысла возиться с заголовком Accept-Language, потому что вы не имеете дело с неизвестным количеством возможных языков в случае «холодного запуска» (когда пользователь впервые открывает сайт с помощью пользовательского запроса).

Вместо этого у вас есть 3 простых, приоритетных варианта:

  1. если установлен сегмент [:language] , используйте его
  2. если $_COOKIE['lang'] установлен, используйте его
  3. использовать язык по умолчанию

Когда у вас есть язык, вы просто пытаетесь перевести запрос, и если перевод не выполняется, используйте «значение по умолчанию» для этого конкретного сегмента (на основе результатов маршрутизации).

Разве здесь нет третьего варианта?

Да, технически вы можете комбинировать оба подхода, но это усложняло бы процесс и предназначалось для людей, которые хотят вручную изменить URL http://site.tld/en/news на http://site.tld/de/news и ожидайте, что страница новостей изменится на немецкий.

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

Какой подход к использованию?

Как вы уже догадались, я бы рекомендовал http://site.tld/[:language]/[:query] как более разумный вариант.

Также в реальной ситуации слова у вас будет 3-я значительная часть в URL: «title». Как и во имя продукта в интернет-магазине или заголовок статьи на новостном сайте.

Пример: http://site.tld/en/news/article/121415/EU-as-global-reserve-currency

В этом случае '/news/article/121415' будет запросом, а 'EU-as-global-reserve-currency' – это заголовок. Чисто для целей SEO.

Можно ли это сделать в Ларавеле?

Kinda, но не по умолчанию.

Я не слишком хорошо знаком с этим, но из того, что я видел, Laravel использует простой механизм маршрутизации на основе шаблонов. Для реализации многоязычных URL-адресов вам, вероятно, придется расширять основные классы (классы) , поскольку для многоязычной маршрутизации необходим доступ к различным формам хранения (база данных, кеш и / или файлы конфигурации).

Он разгромлен. Что теперь?

В результате всего вы получите две ценные части информации: текущий язык и переведенные сегменты запроса. Эти значения затем могут использоваться для отправки в класс (ы), который приведет к результату.

В основном, следующий URL: http://site.tld/ru/blog/novinka (или версия без '/ru' ) превращается во что-то вроде

 $parameters = [ 'language' => 'ru', 'classname' => 'blog', 'method' => 'latest', ]; 

Что вы просто используете для отправки:

 $instance = new {$parameter['classname']}; $instance->{'get'.$parameters['method']}( $parameters ); 

.. или некоторые его варианты, в зависимости от конкретной реализации.

Внедрение i18n без использования производительности Использование предварительного процессора, как это предлагает Томас Блей

На работе мы недавно реализовали реализацию i18n по нескольким нашим свойствам, и одна из вещей, с которыми мы постоянно боролись, – это успех, связанный с переводом на лету, после чего я обнаружил это замечательное сообщение в блоге Томаса Блей которые вдохновили нас на то, как мы используем i18n для обработки больших загрузок трафика с минимальными проблемами производительности.

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

Метки перевода

Томас использует {tr} и {/tr} теги, чтобы определить, где начинаются и заканчиваются переводы. Из-за того, что мы используем TWIG, мы не хотим использовать { чтобы избежать путаницы, поэтому вместо этого используем [%tr%] и [%/tr%] . В принципе, это выглядит так:

 `return [%tr%]formatted_value[%/tr%];` 

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

Файлы INI

Затем мы создаем INI-файл для каждого языка, в формате placeholder = translated :

 // lang/fr.ini formatted_value = number_format($value * Model_Exchange::getEurRate(), 2, ',', ' ') . '€' // lang/en_gb.ini formatted_value = '£' . number_format($value * Model_Exchange::getStgRate()) // lang/en_us.ini formatted_value = '$' . number_format($value) 

Было бы тривиально разрешить пользователю изменять их внутри CMS, просто получить keypairs по preg_split на \n или = и сделать CMS в состоянии писать в INI-файлы.

Компонент предварительного процессора

По сути, Томас предлагает использовать точно так же «компилятор» (хотя, по правде говоря, это препроцессор), как это, чтобы взять ваши файлы перевода и создать статические файлы PHP на диске. Таким образом, мы по существу кэшируем наши переведенные файлы вместо вызова функции перевода для каждой строки в файле:

 // This function was written by Thomas Bley, not by me function translate($file) { $cache_file = 'cache/'.LANG.'_'.basename($file).'_'.filemtime($file).'.php'; // (re)build translation? if (!file_exists($cache_file)) { $lang_file = 'lang/'.LANG.'.ini'; $lang_file_php = 'cache/'.LANG.'_'.filemtime($lang_file).'.php'; // convert .ini file into .php file if (!file_exists($lang_file_php)) { file_put_contents($lang_file_php, '<?php $strings='. var_export(parse_ini_file($lang_file), true).';', LOCK_EX); } // translate .php into localized .php file $tr = function($match) use (&$lang_file_php) { static $strings = null; if ($strings===null) require($lang_file_php); return isset($strings[ $match[1] ]) ? $strings[ $match[1] ] : $match[1]; }; // replace all {t}abc{/t} by tr() file_put_contents($cache_file, preg_replace_callback( '/\[%tr%\](.*?)\[%\/tr%\]/', $tr, file_get_contents($file)), LOCK_EX); } return $cache_file; } 

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

Как позвонить

Опять же, этот пример от Томаса Блей, а не от меня:

 // instead of require("core/example.php"); echo (new example())->now(); // we write define('LANG', 'en_us'); require(translate('core/example.php')); echo (new example())->now(); 

Мы сохраняем язык в файле cookie (или переменной сеанса, если мы не можем получить файл cookie), а затем извлекаем его для каждого запроса. Вы можете объединить это с необязательным параметром $_GET чтобы переопределить язык, но я не предлагаю subdomain-per-language или page-per-language, потому что это затруднит просмотр страниц, которые будут популярны, и уменьшит значение входящих ссылок, поскольку они будут иметь их чуть более распространенные.

Зачем использовать этот метод?

Нам нравится этот метод предварительной обработки по трем причинам:

  1. Огромный прирост производительности за счет того, что вы не вызываете целую кучу функций для контента, который редко изменяется (с этой системой, 100 тыс. Посетителей на французском языке по-прежнему будут заканчиваться только заменой перевода).
  2. Он не добавляет никакой нагрузки в нашу базу данных, поскольку он использует простые плоские файлы и является чистым PHP-решением.
  3. Возможность использования выражений PHP в наших переводах.

Получение переведенного содержимого базы данных

Мы просто добавляем столбец для контента в нашу базу данных, называемый language , тогда мы используем метод доступа для константы LANG который мы определили ранее, поэтому наши вызовы SQL (с использованием ZF1, к сожалению) выглядят следующим образом:

 $query = select()->from($this->_name) ->where('language = ?', User::getLang()) ->where('id = ?', $articleId) ->limit(1); 

Наши статьи содержат сложный первичный ключ по id и language поэтому статья 54 может существовать на всех языках. В нашем LANG умолчанию используется значение en_US если не указано.

URL Slug Translation

Я бы объединил две вещи здесь: одна функция в вашем бутстрапе, которая принимает параметр $_GET для языка и переопределяет переменную cookie, а другая – маршрутизацию, которая принимает несколько слизней. Тогда вы можете сделать что-то подобное в своей маршрутизации:

 "/wilkommen" => "/welcome/lang/de" ... etc ... 

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

Примечания относительно нескольких других опций

PHP-on-the-Fly Translation

Я не вижу, что они предлагают преимущество перед предварительно обработанными переводами.

Переводы на основе интерфейса

Я давно нашел их интересными, но есть несколько предостережений. Например, вы должны предоставить пользователю полный список фраз на своем веб-сайте, который вы планируете переводить, это может быть проблематично, если есть области сайта, которые вы скрываете или не разрешаете им получать доступ.

Вы также должны предположить, что все ваши пользователи готовы и могут использовать Javascript на вашем сайте, но из моей статистики около 2,5% наших пользователей работают без него (или используя Noscript, чтобы блокировать использование наших сайтов) ,

Переводы, управляемые базой данных

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

Я предлагаю вам не изобретать колесо, а использовать список атрибутов gettext и ISO. Вы видели, как i18n / l10n реализованы в популярных CMS или фреймворках?

Используя gettext, у вас будет мощный инструмент, в котором многие случаи уже реализованы как множественные формы чисел. На английском языке у вас есть только 2 варианта: единственное и множественное число. Но на русском языке, например, есть 3 формы, и это не так просто, как на английском.

Также многие переводчики уже имеют опыт работы с gettext.

Взгляните на CakePHP или Drupal . Оба многоязычных включены. CakePHP в качестве примера локализации интерфейса и Drupal в качестве примера преобразования контента.

Для l10n использование базы данных совсем не так. Это будет тонны по запросам. Стандартный подход заключается в том, чтобы получить все данные l10n в памяти на ранней стадии (или во время первого вызова функции i10n, если вы предпочитаете ленивую загрузку). Это может быть чтение из файла .po или из базы данных DB сразу. А чем просто читать запрошенные строки из массива.

Если вам нужно внедрить онлайн-инструмент для перевода интерфейса, вы можете иметь все эти данные в БД, но не сохранять все данные в файл для работы с ним. Чтобы уменьшить объем данных в памяти, вы можете разделить все переведенные сообщения / строки на группы и загружать только те группы, которые вам нужны, если это будет возможно.

Таким образом, вы полностью правы в своем # 3. За одним исключением: обычно это один большой файл, а не файл для каждого контроллера. Потому что для производительности лучше всего открыть один файл. Вероятно, вы знаете, что некоторые высокопроизводительные веб-приложения компилируют весь PHP-код в один файл, чтобы избежать операций с файлами при вызове include / require.

О URL-адресах. Google косвенно предлагает использовать перевод:

четко указать французский контент: http://example.ca/fr/vélo-de-montagne.html

Также я думаю, что вам нужно перенаправить пользователя на префикс языка по умолчанию, например http://examlpe.com/about-us будет перенаправляться на http://examlpe.com/en/about-us. Но если ваш сайт использует только один язык, чтобы вы вообще не нужны префиксы.

Выезд: http://www.audiomicro.com/trailer-hit-impact-psychodrama-sound-effects-836925 http://nl.audiomicro.com/aanhangwagen-hit-effect-psychodrama-geluidseffecten-836925 http: / / /de.audiomicro.com/anhanger-hit-auswirkungen-psychodrama-sound-effekte-836925

Перевод содержимого – более сложная задача. Я думаю, что это будут некоторые различия с различными типами контента, например, статьи, пункты меню и т. Д. Но в №4 вы в правильном направлении. Взгляните на Drupal, чтобы иметь больше идей. Он имеет достаточно четкую схему БД и достаточно хороший интерфейс для перевода. Как вы создаете статью и выбираете язык для нее. И чем позже вы сможете перевести его на другие языки.

Интерфейс перевода Drupal

Я думаю, что это не проблема с URL-пулями. Вы можете просто создать отдельную таблицу для пули, и это будет правильным решением. Также, используя правильные индексы, не стоит запрашивать таблицу даже с огромным количеством данных. И это был не полный текстовый поиск, а совпадение строк, если будет использовать тип данных varchar для slug, и вы также можете иметь индекс в этом поле.

PS Извините, мой английский далеко не идеален.

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

Допустим, у вас есть этот текст:

 Welcome! 

Вы можете ввести это в базу данных с переводами, но вы также можете сделать это:

 $welcome = array( "English"=>"Welcome!", "German"=>"Willkommen!", "French"=>"Bienvenue!", "Turkish"=>"Hoşgeldiniz!", "Russian"=>"Добро пожаловать!", "Dutch"=>"Welkom!", "Swedish"=>"Välkommen!", "Basque"=>"Ongietorri!", "Spanish"=>"Bienvenito!" "Welsh"=>"Croeso!"); 

Теперь, если ваш сайт использует файл cookie, у вас есть это, например:

 $_COOKIE['language']; 

Чтобы было проще, давайте преобразуем его в код, который можно легко использовать:

 $language=$_COOKIE['language']; 

Если ваш язык cookie валлийский, и у вас есть этот код:

 echo $welcome[$language]; 

Результатом этого будет:

 Croeso! 

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

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

Я раньше сталкивался с подобной проблемой и написал следующий класс, чтобы решить мою проблему

Объект: Locale \ Locale

 <?php namespace Locale; class Locale{ // Following array stolen from Zend Framework public $country_to_locale = array( 'AD' => 'ca_AD', 'AE' => 'ar_AE', 'AF' => 'fa_AF', 'AG' => 'en_AG', 'AI' => 'en_AI', 'AL' => 'sq_AL', 'AM' => 'hy_AM', 'AN' => 'pap_AN', 'AO' => 'pt_AO', 'AQ' => 'und_AQ', 'AR' => 'es_AR', 'AS' => 'sm_AS', 'AT' => 'de_AT', 'AU' => 'en_AU', 'AW' => 'nl_AW', 'AX' => 'sv_AX', 'AZ' => 'az_Latn_AZ', 'BA' => 'bs_BA', 'BB' => 'en_BB', 'BD' => 'bn_BD', 'BE' => 'nl_BE', 'BF' => 'mos_BF', 'BG' => 'bg_BG', 'BH' => 'ar_BH', 'BI' => 'rn_BI', 'BJ' => 'fr_BJ', 'BL' => 'fr_BL', 'BM' => 'en_BM', 'BN' => 'ms_BN', 'BO' => 'es_BO', 'BR' => 'pt_BR', 'BS' => 'en_BS', 'BT' => 'dz_BT', 'BV' => 'und_BV', 'BW' => 'en_BW', 'BY' => 'be_BY', 'BZ' => 'en_BZ', 'CA' => 'en_CA', 'CC' => 'ms_CC', 'CD' => 'sw_CD', 'CF' => 'fr_CF', 'CG' => 'fr_CG', 'CH' => 'de_CH', 'CI' => 'fr_CI', 'CK' => 'en_CK', 'CL' => 'es_CL', 'CM' => 'fr_CM', 'CN' => 'zh_Hans_CN', 'CO' => 'es_CO', 'CR' => 'es_CR', 'CU' => 'es_CU', 'CV' => 'kea_CV', 'CX' => 'en_CX', 'CY' => 'el_CY', 'CZ' => 'cs_CZ', 'DE' => 'de_DE', 'DJ' => 'aa_DJ', 'DK' => 'da_DK', 'DM' => 'en_DM', 'DO' => 'es_DO', 'DZ' => 'ar_DZ', 'EC' => 'es_EC', 'EE' => 'et_EE', 'EG' => 'ar_EG', 'EH' => 'ar_EH', 'ER' => 'ti_ER', 'ES' => 'es_ES', 'ET' => 'en_ET', 'FI' => 'fi_FI', 'FJ' => 'hi_FJ', 'FK' => 'en_FK', 'FM' => 'chk_FM', 'FO' => 'fo_FO', 'FR' => 'fr_FR', 'GA' => 'fr_GA', 'GB' => 'en_GB', 'GD' => 'en_GD', 'GE' => 'ka_GE', 'GF' => 'fr_GF', 'GG' => 'en_GG', 'GH' => 'ak_GH', 'GI' => 'en_GI', 'GL' => 'iu_GL', 'GM' => 'en_GM', 'GN' => 'fr_GN', 'GP' => 'fr_GP', 'GQ' => 'fan_GQ', 'GR' => 'el_GR', 'GS' => 'und_GS', 'GT' => 'es_GT', 'GU' => 'en_GU', 'GW' => 'pt_GW', 'GY' => 'en_GY', 'HK' => 'zh_Hant_HK', 'HM' => 'und_HM', 'HN' => 'es_HN', 'HR' => 'hr_HR', 'HT' => 'ht_HT', 'HU' => 'hu_HU', 'ID' => 'id_ID', 'IE' => 'en_IE', 'IL' => 'he_IL', 'IM' => 'en_IM', 'IN' => 'hi_IN', 'IO' => 'und_IO', 'IQ' => 'ar_IQ', 'IR' => 'fa_IR', 'IS' => 'is_IS', 'IT' => 'it_IT', 'JE' => 'en_JE', 'JM' => 'en_JM', 'JO' => 'ar_JO', 'JP' => 'ja_JP', 'KE' => 'en_KE', 'KG' => 'ky_Cyrl_KG', 'KH' => 'km_KH', 'KI' => 'en_KI', 'KM' => 'ar_KM', 'KN' => 'en_KN', 'KP' => 'ko_KP', 'KR' => 'ko_KR', 'KW' => 'ar_KW', 'KY' => 'en_KY', 'KZ' => 'ru_KZ', 'LA' => 'lo_LA', 'LB' => 'ar_LB', 'LC' => 'en_LC', 'LI' => 'de_LI', 'LK' => 'si_LK', 'LR' => 'en_LR', 'LS' => 'st_LS', 'LT' => 'lt_LT', 'LU' => 'fr_LU', 'LV' => 'lv_LV', 'LY' => 'ar_LY', 'MA' => 'ar_MA', 'MC' => 'fr_MC', 'MD' => 'ro_MD', 'ME' => 'sr_Latn_ME', 'MF' => 'fr_MF', 'MG' => 'mg_MG', 'MH' => 'mh_MH', 'MK' => 'mk_MK', 'ML' => 'bm_ML', 'MM' => 'my_MM', 'MN' => 'mn_Cyrl_MN', 'MO' => 'zh_Hant_MO', 'MP' => 'en_MP', 'MQ' => 'fr_MQ', 'MR' => 'ar_MR', 'MS' => 'en_MS', 'MT' => 'mt_MT', 'MU' => 'mfe_MU', 'MV' => 'dv_MV', 'MW' => 'ny_MW', 'MX' => 'es_MX', 'MY' => 'ms_MY', 'MZ' => 'pt_MZ', 'NA' => 'kj_NA', 'NC' => 'fr_NC', 'NE' => 'ha_Latn_NE', 'NF' => 'en_NF', 'NG' => 'en_NG', 'NI' => 'es_NI', 'NL' => 'nl_NL', 'NO' => 'nb_NO', 'NP' => 'ne_NP', 'NR' => 'en_NR', 'NU' => 'niu_NU', 'NZ' => 'en_NZ', 'OM' => 'ar_OM', 'PA' => 'es_PA', 'PE' => 'es_PE', 'PF' => 'fr_PF', 'PG' => 'tpi_PG', 'PH' => 'fil_PH', 'PK' => 'ur_PK', 'PL' => 'pl_PL', 'PM' => 'fr_PM', 'PN' => 'en_PN', 'PR' => 'es_PR', 'PS' => 'ar_PS', 'PT' => 'pt_PT', 'PW' => 'pau_PW', 'PY' => 'gn_PY', 'QA' => 'ar_QA', 'RE' => 'fr_RE', 'RO' => 'ro_RO', 'RS' => 'sr_Cyrl_RS', 'RU' => 'ru_RU', 'RW' => 'rw_RW', 'SA' => 'ar_SA', 'SB' => 'en_SB', 'SC' => 'crs_SC', 'SD' => 'ar_SD', 'SE' => 'sv_SE', 'SG' => 'en_SG', 'SH' => 'en_SH', 'SI' => 'sl_SI', 'SJ' => 'nb_SJ', 'SK' => 'sk_SK', 'SL' => 'kri_SL', 'SM' => 'it_SM', 'SN' => 'fr_SN', 'SO' => 'sw_SO', 'SR' => 'srn_SR', 'ST' => 'pt_ST', 'SV' => 'es_SV', 'SY' => 'ar_SY', 'SZ' => 'en_SZ', 'TC' => 'en_TC', 'TD' => 'fr_TD', 'TF' => 'und_TF', 'TG' => 'fr_TG', 'TH' => 'th_TH', 'TJ' => 'tg_Cyrl_TJ', 'TK' => 'tkl_TK', 'TL' => 'pt_TL', 'TM' => 'tk_TM', 'TN' => 'ar_TN', 'TO' => 'to_TO', 'TR' => 'tr_TR', 'TT' => 'en_TT', 'TV' => 'tvl_TV', 'TW' => 'zh_Hant_TW', 'TZ' => 'sw_TZ', 'UA' => 'uk_UA', 'UG' => 'sw_UG', 'UM' => 'en_UM', 'US' => 'en_US', 'UY' => 'es_UY', 'UZ' => 'uz_Cyrl_UZ', 'VA' => 'it_VA', 'VC' => 'en_VC', 'VE' => 'es_VE', 'VG' => 'en_VG', 'VI' => 'en_VI', 'VN' => 'vn_VN', 'VU' => 'bi_VU', 'WF' => 'wls_WF', 'WS' => 'sm_WS', 'YE' => 'ar_YE', 'YT' => 'swb_YT', 'ZA' => 'en_ZA', 'ZM' => 'en_ZM', 'ZW' => 'sn_ZW' ); /** * Store the transaltion for specific languages * * @var array */ protected $translation = array(); /** * Current locale * * @var string */ protected $locale; /** * Default locale * * @var string */ protected $default_locale; /** * * @var string */ protected $locale_dir; /** * Construct. * * * @param string $locale_dir */ public function __construct($locale_dir) { $this->locale_dir = $locale_dir; } /** * Set the user define localte * * @param string $locale */ public function setLocale($locale = null) { $this->locale = $locale; return $this; } /** * Get the user define locale * * @return string */ public function getLocale() { return $this->locale; } /** * Get the Default locale * * @return string */ public function getDefaultLocale() { return $this->default_locale; } /** * Set the default locale * * @param string $locale */ public function setDefaultLocale($locale) { $this->default_locale = $locale; return $this; } /** * Determine if transltion exist or translation key exist * * @param string $locale * @param string $key * @return boolean */ public function hasTranslation($locale, $key = null) { if (null == $key && isset($this->translation[$locale])) { return true; } elseif (isset($this->translation[$locale][$key])) { return true; } return false; } /** * Get the transltion for required locale or transtion for key * * @param string $locale * @param string $key * @return array */ public function getTranslation($locale, $key = null) { if (null == $key && $this->hasTranslation($locale)) { return $this->translation[$locale]; } elseif ($this->hasTranslation($locale, $key)) { return $this->translation[$locale][$key]; } return array(); } /** * Set the transtion for required locale * * @param string $locale * Language code * @param string $trans * translations array */ public function setTranslation($locale, $trans = array()) { $this->translation[$locale] = $trans; } /** * Remove transltions for required locale * * @param string $locale */ public function removeTranslation($locale = null) { if (null === $locale) { unset($this->translation); } else { unset($this->translation[$locale]); } } /** * Initialize locale * * @param string $locale */ public function init($locale = null, $default_locale = null) { // check if previously set locale exist or not $this->init_locale(); if ($this->locale != null) { return; } if ($locale == null || (! preg_match('#^[az]+_[a-zA-Z_]+$#', $locale) && ! preg_match('#^[az]+_[a-zA-Z]+_[a-zA-Z_]+$#', $locale))) { $this->detectLocale(); } else { $this->locale = $locale; } $this->init_locale(); } /** * Attempt to autodetect locale * * @return void */ private function detectLocale() { $locale = false; // GeoIP if (function_exists('geoip_country_code_by_name') && isset($_SERVER['REMOTE_ADDR'])) { $country = geoip_country_code_by_name($_SERVER['REMOTE_ADDR']); if ($country) { $locale = isset($this->country_to_locale[$country]) ? $this->country_to_locale[$country] : false; } } // Try detecting locale from browser headers if (! $locale) { if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { $languages = explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']); foreach ($languages as $lang) { $lang = str_replace('-', '_', trim($lang)); if (strpos($lang, '_') === false) { if (isset($this->country_to_locale[strtoupper($lang)])) { $locale = $this->country_to_locale[strtoupper($lang)]; } } else { $lang = explode('_', $lang); if (count($lang) == 3) { // language_Encoding_COUNTRY $this->locale = strtolower($lang[0]) . ucfirst($lang[1]) . strtoupper($lang[2]); } else { // language_COUNTRY $this->locale = strtolower($lang[0]) . strtoupper($lang[1]); } return; } } } } // Resort to default locale specified in config file if (! $locale) { $this->locale = $this->default_locale; } } /** * Check if config for selected locale exists * * @return void */ private function init_locale() { if (! file_exists(sprintf('%s/%s.php', $this->locale_dir, $this->locale))) { $this->locale = $this->default_locale; } } /** * Load a Transtion into array * * @return void */ private function loadTranslation($locale = null, $force = false) { if ($locale == null) $locale = $this->locale; if (! $this->hasTranslation($locale)) { $this->setTranslation($locale, include (sprintf('%s/%s.php', $this->locale_dir, $locale))); } } /** * Translate a key * * @param * string Key to be translated * @param * string optional arguments * @return string */ public function translate($key) { $this->init(); $this->loadTranslation($this->locale); if (! $this->hasTranslation($this->locale, $key)) { if ($this->locale !== $this->default_locale) { $this->loadTranslation($this->default_locale); if ($this->hasTranslation($this->default_locale, $key)) { $translation = $this->getTranslation($this->default_locale, $key); } else { // return key as it is or log error here return $key; } } else { return $key; } } else { $translation = $this->getTranslation($this->locale, $key); } // Replace arguments if (false !== strpos($translation, '{a:')) { $replace = array(); $args = func_get_args(); for ($i = 1, $max = count($args); $i < $max; $i ++) { $replace['{a:' . $i . '}'] = $args[$i]; } // interpolate replacement values into the messsage then return return strtr($translation, $replace); } return $translation; } } 

Применение

  <?php ## /locale/en.php return array( 'name' => 'Hello {a:1}' 'name_full' => 'Hello {a:1} {a:2}' ); $locale = new Locale(__DIR__ . '/locale'); $locale->setLocale('en');// load en.php from locale dir //want to work with auto detection comment $locale->setLocale('en'); echo $locale->translate('name', 'Foo'); echo $locale->translate('name', 'Foo', 'Bar'); 

Как это работает

{a:1} is replaced by 1st argument passed to method Locale::translate('key_name','arg1') {a:2} is replaced by 2nd argument passed to method Locale::translate('key_name','arg1','arg2')

How detection works

  • By default if geoip is installed then it will return country code by geoip_country_code_by_name and if geoip is not installed the fallback to HTTP_ACCEPT_LANGUAGE header

Just a sub answer: Absolutely use translated urls with a language identifier in front of them: http://www.domain.com/nl/over-ons
Hybride solutions tend to get complicated, so I would just stick with it. Зачем? Cause the url is essential for SEO.

About the db translation: Is the number of languages more or less fixed? Or rather unpredictable and dynamic? If it is fixed, I would just add new columns, otherwise go with multiple tables.

But generally, why not use Drupal? I know everybody wants to build their own CMS cause it's faster, leaner, etc. etc. But that is just really a bad idea!

I had the same probem a while ago, before starting using Symfony framework.

  1. Just use a function __() which has arameters pageId (or objectId, objectTable described in #2), target language and an optional parameter of fallback (default) language. The default language could be set in some global config in order to have an easier way to change it later.

  2. For storing the content in database i used following structure: (pageId, language, content, variables).

    • pageId would be a FK to your page you want to translate. if you have other objects, like news, galleries or whatever, just split it into 2 fields objectId, objectTable.

    • language – obviously it would store the ISO language string EN_en, LT_lt, EN_us etc.

    • content – the text you want to translate together with the wildcards for variable replacing. Example "Hello mr. %%name%%. Your account balance is %%balance%%."

    • variables – the json encoded variables. PHP provides functions to quickly parse these. Example "name: Laurynas, balance: 15.23".

    • you mentioned also slug field. you could freely add it to this table just to have a quick way to search for it.

  3. Your database calls must be reduced to minimum with caching the translations. It must be stored in PHP array, because it is the fastest structure in PHP language. How you will make this caching is up to you. From my experience you should have a folder for each language supported and an array for each pageId. The cache should be rebuilt after you update the translation. ONLY the changed array should be regenerated.

  4. i think i answered that in #2

  5. your idea is perfectly logical. this one is pretty simple and i think will not make you any problems.

URLs should be translated using the stored slugs in the translation table.

Final words

it is always good to research the best practices, but do not reinvent the wheel. just take and use the components from well known frameworks and use them.

take a look at Symfony translation component . It could be a good code base for you.

I am not going to attempt to refine the answers already given. Instead I will tell you about the way my own OOP PHP framework handles translations.

Internally, my framework use codes like en, fr, es, cn and so on. An array holds the languages supported by the website: array('en','fr','es','cn') The language code is passed via $_GET (lang=fr) and if not passed or not valid, it is set to the first language in the array. So at any time during program execution and from the very beginning, the current language is known.

It is useful to understand the kind of content that needs to be translated in a typical application:

1) error messages from classes (or procedural code) 2) non-error messages from classes (or procedural code) 3) page content (usually store in a database) 4) site-wide strings (like website name) 5) script-specific strings

The first type is simple to understand. Basically, we are talking about messages like "could not connect to the database …". These messages only need to be loaded when an error occurs. My manager class receives a call from the other classes and using the information passed as parameters simply goes to relevant the class folder and retrieves the error file.

The second type of error message is more like the messages you get when the validation of a form went wrong. ("You cannot leave … blank" or "please choose a password with more than 5 characters"). The strings need to be loaded before the class runs.I know what is

For the actual page content, I use one table per language, each table prefixed by the code for the language. So en_content is the table with English language content, es_content is for spain, cn_content for China and fr_content is the French stuff.

The fourth kind of string is relevant throughout your website. This is loaded via a configuration file named using the code for the language, that is en_lang.php, es_lang.php and so on. In the global language file you will need to load the translated languages such as array('English','Chinese', 'Spanish','French') in the English global file and array('Anglais','Chinois', 'Espagnol', 'Francais') in the French file. So when you populate a dropdown for language selection, it is in the correct language 😉

Finally you have the script-specific strings. So if you write a cooking application, it might be "Your oven was not hot enough".

In my application cycle, the global language file is loaded first. In there you will find not just global strings (like "Jack's Website") but also settings for some of the classes. Basically anything that is language or culture-dependent. Some of the strings in there include masks for dates (MMDDYYYY or DDMMYYYY), or ISO Language Codes. In the main language file, I include strings for individual classes becaue there are so few of them.

The second and last language file that is read from disk is the script language file. lang_en_home_welcome.php is the language file for the home/welcome script. A script is defined by a mode (home) and an action (welcome). Each script has its own folder with config and lang files.

The script pulls the content from the database naming the content table as explained above.

If something goes wrong, the manager knows where to get the language-dependent error file. That file is only loaded in case of an error.

So the conclusion is obvious. Think about the translation issues before you start developing an application or framework. You also need a development workflow that incorporates translations. With my framework, I develop the whole site in English and then translate all the relevant files.

Just a quick final word on the way the translation strings are implemented. My framework has a single global, the $manager, which runs services available to any other service. So for example the form service gets hold of the html service and uses it to write the html. One of the services on my system is the translator service. $translator->set($service,$code,$string) sets a string for the current language. The language file is a list of such statements. $translator->get($service,$code) retrieves a translation string. The $code can be numeric like 1 or a string like 'no_connection'. There can be no clash between services because each has its own namespace in the translator's data area.

I post this here in the hope it will save somebody the task of reinventing the wheel like I had to do a few long years ago.

I've been asking myself related questions over and over again, then got lost in formal languages… but just to help you out a little I'd like to share some findings:

I recommend to give a look at advanced CMS

Typo3 for PHP (I know there is a lot of stuff but thats the one I think is most mature)

Plone in Python

If you find out that the web in 2013 should work different then, start from scratch. That would mean to put together a team of highly skilled/experienced people to build a new CMS. May be you'd like to give a look at polymer for that purpose.

If it comes to coding and multilingual websites / native language support, I think every programmer should have a clue about unicode. If you don't know unicode you'll most certainly mess up your data. Do not go with the thousands of ISO codes. They'll only save you some memory. But you can do literally everything with UTF-8 even store chinese chars. But for that you'd need to store either 2 or 4 byte chars that makes it basically a utf-16 or utf-32.

If it's about URL encoding, again there you shouldn't mix encodings and be aware that at least for the domainname there are rules defined by different lobbies that provide applications like a browser. eg a Domain could be very similar like:

ьankofamerica.com or bankofamerica.com samesamebutdifferent 😉

Of course you need the filesystem to work with all encodings. Another plus for unicode using utf-8 filesystem.

If its about translations, think about the structure of documents. eg a book or an article. You have the docbook specifications to understand about those structures. But in HTML its just about content blocks. So you'd like to have a translation on that level, also on webpage level or domain level. So if a block doesn't exist its just not there, if a webpage doesn't exist you'll get redirected to the upper navigation level. If a domain should be completely different in navigation structure, then.. its a complete different structure to manage. This can already be done with Typo3.

If its about frameworks, the most mature ones I know, to do the general stuff like MVC(buzzword I really hate it! Like "performance" If you want to sell something, use the word performance and featurerich and you sell… what the hell) is Zend . It has proven to be a good thing to bring standards to php chaos coders. But, typo3 also has a Framework besides the CMS. Recently it has been redeveloped and is called flow3 now. The frameworks of course cover database abstraction, templating and concepts for caching, but have individual strengths.

If its about caching… that can be awefully complicated / multilayered. In PHP you'll think about accellerator, opcode, but also html, httpd, mysql, xml, css, js … any kinds of caches. Of course some parts should be cached and dynamic parts like blog answers shouldn't. Some should be requested over AJAX with generated urls. JSON, hashbangs etc.

Then, you'd like to have any little component on your website to be accessed or managed only by certain users , so conceptually that plays a big role.

Also you'd like to make statistics , maybe have distributed system / a facebook of facebooks etc. any software to be built on top of your over the top cms … so you need different type of databases inmemory, bigdata, xml, whatsoever.

well, I think thats enough for now. If you haven't heard of either typo3 / plone or mentioned frameworks, you have enough to study. On that path you'll find a lot of solutions for questions you haven't asked yet.

If then you think, lets make a new CMS because its 2013 and php is about to die anyway, then you r welcome to join any other group of developers hopefully not getting lost.

Удачи!

И кстати. how about people will not having any websites anymore in the future? and we'll all be on google+? I hope developers become a little more creative and do something usefull(to not be assimilated by the borgle)

//// Edit /// Just a little thought for your existing application:

If you have a php mysql CMS and you wanted to embed multilang support. you could either use your table with an aditional column for any language or insert the translation with an object id and a language id in the same table or create an identical table for any language and insert objects there, then make a select union if you want to have them all displayed. For the database use utf8 general ci and of course in the front/backend use utf8 text/encoding. I have used url path segments for urls in the way you already explaned like

domain.org/en/about you can map the lang ID to your content table. anyway you need to have a map of parameters for your urls so you'd like to define a parameter to be mapped from a pathsegment in your URL that would be eg

domain.org/en/about/employees/IT/administrators/

lookup configuration

pageid| url

1 | /about/employees/../..

1 | /../about/employees../../

map parameters to url pathsegment ""

 $parameterlist[lang] = array(0=>"nl",1=>"en"); // default nl if 0 $parameterlist[branch] = array(1=>"IT",2=>"DESIGN"); // default nl if 0 $parameterlist[employertype] = array(1=>"admin",1=>"engineer"); //could be a sql result $websiteconfig[]=$userwhatever; $websiteconfig[]=$parameterlist; $someparameterlist[] = array("branch"=>$someid); $someparameterlist[] = array("employertype"=>$someid); function getURL($someparameterlist){ // todo foreach someparameter lookup pathsegment return path; } 

per say, thats been covered already in upper post.

And to not forget, you'd need to "rewrite" the url to your generating php file that would in most cases be index.php

Database work:

Create Language Table 'languages':

Fields:

language_id(primary and auto increamented)

language_name

created_at

created_by

updated_at

updated_by

Create a table in database 'content':

Fields:

content_id(primary and auto increamented)

main_content

header_content

footer_content

leftsidebar_content

rightsidebar_content

language_id(foreign key: referenced to languages table)

created_at

created_by

updated_at

updated_by

Front End Work:

When user selects any language from dropdown or any area then save selected language id in session like,

$_SESSION['language']=1;

Now fetch data from database table 'content' based on language id stored in session.

Detail may found here http://skillrow.com/multilingual-website-in-php-2/

As a person who live in Quebec where almost all site is french and english… i have try many if not most multilanguage plugin for WP… the one an only usefull solution that work nive with all my site is mQtranslate… i live and die with it !

https://wordpress.org/plugins/mqtranslate/

A really simple option that works with any website where you can upload Javascript is http://www.multilingualizer.com

It lets you put all text for all languages onto one page and then hides the languages the user doesn't need to see. Работает хорошо.

What about WORDPRESS + MULTI-LANGUAGE SITE BASIS (plugin) ? the site will have structure:

  • example.com/ eng /category1/….
  • example.com/ eng /my-page….
  • example.com/ rus /category1/….
  • example.com/ rus /my-page….

The plugin provides Interface for Translation all phrases, with simple logic:

 (ENG) my_title - "Hello user" (SPA) my_title - "Holla usuario" 

then it can be outputed:
echo translate('my_title', LNG); // LNG is auto-detected

ps however, check, if the plugin is still active.