Лучшая практика многоязычного сайта

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

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

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

Что должно быть переведено

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

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

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

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

  1. Разобранные PHP-шаблоны: система шаблонов должна быть проанализирована PHP. Таким образом, я могу вставить переведенные параметры в HTML без необходимости открывать шаблоны и изменять их. Кроме того, синтаксический анализ PHP дает мне возможность иметь 1 шаблон для всего сайта, а не иметь подпапку для каждого языка (который я имел раньше). Методом достижения этой цели может быть Smarty, TemplatePower, Laravel’s Blade или любой другой анализатор шаблонов. Как я уже сказал, это должно быть независимым от письменного решения.
  2. База данных управляетсяВозможно, мне не нужно упоминать это снова. Но решение должно основываться на базе данных. CMS предназначена для объектно-ориентированного и MVC, поэтому мне нужно подумать о логической структуре данных для строк. Поскольку мои шаблоны будут структурированы: templates / Controller / View.php, возможно, эта структура будет иметь больше смысла: Controller.View.parameter, Таблица базы данных будет иметь эти поля длинной с value поле. Внутри шаблонов мы могли бы использовать какой-то метод сортировки, такой как echo __('Controller.View.welcome', array('name', 'Joshua')) и параметр содержит Welcome, :name, Таким образом, результат Welcome, Joshua, Это кажется хорошим способом сделать это, потому что такие параметры, как: name, легко понять редактору.
  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 (ссылается на английскую версию новости с идентификатором = 1). Но у этого метода есть два огромных недостатка: во-первых, эта таблица имеет тенденцию получать довольно много времени с большим количеством данных в базе данных, и, во-вторых, использование этой установки для поиска в таблице было бы адской работой. Например. поиск слагаемого SEO для этого элемента будет полнотекстовым поиском, что довольно глупо. Но с другой стороны: это быстрый способ очень быстро создавать переводимый контент в каждой таблице, но я не верю, что этот профессионал перевешивает доводы «против».
  5. Front-end WorkКроме того, клиенту нужно подумать. Конечно, мы будем хранить доступные языки в базе данных и (де) активировать те, которые нам нужны. Таким образом, сценарий может создать раскрывающийся список для выбора языка, а серверная часть может автоматически решить, какие переводы можно выполнить с помощью CMS. Выбранный язык (например, en_EN) будет затем использоваться при получении языкового файла для просмотра или для получения правильного перевода для элемента контента на веб-сайте.

Итак, они есть. Мои идеи пока. Они даже не включают в себя опции локализации для дат и т. Д., Но, поскольку мой сервер поддерживает PHP5.3.2 +, лучшим вариантом является использование расширения intl, как описано здесь: 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-идентификации-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:

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

158

Решение

Предпосылка темы

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

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

Хотя все они связаны между собой по-разному, с точки зрения CMS они управляются с использованием различных элементов пользовательского интерфейса и хранятся по-разному. Вы, кажется, уверены в своей реализации и понимании первых двух. Вопрос был о последнем аспекте — «Перевод 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] используется только для идентификации контента

Запрос Α и Ω ..

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

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

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

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

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

Взять, к примеру: 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: «заголовок». Как в названии товара в интернет-магазине, так и в заголовке статьи на новостном сайте.

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

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

Это можно сделать в Ларавеле?

Вроде, но не по умолчанию.

Я не слишком знаком с этим, но из того, что я видел, 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 );

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

101

Другие решения

На работе мы недавно реализовали 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, просто получить пары ключей 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 параметр для переопределения языка, но я не предлагаю субдомен на язык или страницу на язык, потому что это усложнит просмотр популярных страниц и уменьшит ценность входящих ссылок, поскольку они у вас будут более редко распространяется.

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

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

  1. Огромный выигрыш в производительности, если не вызывать целую кучу функций для контента, который редко меняется (с этой системой 100 000 посетителей на французском языке все равно в конечном итоге выполнят замену перевода только один раз).
  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

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

Front-end основанные переводы

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

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

Переводы на основе базы данных

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

48

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

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

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

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

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

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

Так что вы совершенно правы в своем # 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-слизнями. Вы можете просто создать отдельную таблицу для слизней, и это будет правильным решением. Также, используя правильные индексы, нет проблем с запросом таблицы даже с огромным количеством данных.
И это был не полнотекстовый поиск, а совпадение строк, если для slug будет использоваться тип данных varchar, и у вас также может быть индекс для этого поля.

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

13

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

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

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['language'];

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

$language=$_COOKIE['language'];

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

echo $welcome[$language];

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

Croeso!

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

9

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

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

Объект: 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('#^[a-z]+_[a-zA-Z_]+$#', $locale) && ! preg_match('#^[a-z]+_[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} заменяется первым аргументом, переданным методу Locale::translate('key_name','arg1')
{a:2} заменяется вторым аргументом, переданным методу Locale::translate('key_name','arg1','arg2')

Как работает обнаружение

  • По умолчанию, если geoip будет установлен код страны geoip_country_code_by_name и если geoip не установлен, запасной вариант для HTTP_ACCEPT_LANGUAGE заголовок
6

Просто дополнительный ответ:
Абсолютно используйте переведенные URL с идентификатором языка перед ними: http://www.domain.com/nl/over-ons
Гибридные решения, как правило, усложняются, поэтому я просто придерживаюсь этого. Зачем? Потому что URL имеет важное значение для SEO.

О переводе БД: Количество языков более или менее фиксировано? Или скорее непредсказуемый и динамичный? Если это будет исправлено, я просто добавлю новые столбцы, в противном случае использовать несколько таблиц.

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

4

У меня была такая же проблема некоторое время назад, прежде чем начать использовать Symfony фреймворк.

  1. Просто используйте функцию __ (), которая имеет arameters pageId (или objectId, objectTable, описанный в # 2), целевой язык и необязательный параметр резервного языка (по умолчанию). Язык по умолчанию может быть установлен в некоторых глобальных конфигурациях, чтобы иметь более простой способ изменить его позже.

  2. Для хранения контента в базе данных я использовал следующую структуру: (pageId, language, content, variable).

    • pageId будет FK для вашей страницы, которую вы хотите перевести. если у вас есть другие объекты, такие как новости, галереи или что-то еще, просто разбейте его на 2 поля: objectId, objectTable.

    • язык — очевидно, он будет хранить строку языка ISO EN_en, LT_lt, EN_us и т. д.

    • content — текст, который вы хотите перевести вместе с подстановочными знаками для замены переменных. Пример «Здравствуйте, г-н. %% name %%. Баланс вашего счета равен %% balance %%.»

    • переменные — json-кодированные переменные. PHP предоставляет функции для быстрого их анализа. Пример «имя: Лауринас, баланс: 15.23».

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

  3. Вызовы вашей базы данных должны быть сведены к минимуму с кэшированием переводов. Он должен храниться в массиве PHP, потому что это самая быстрая структура в языке PHP. Как вы сделаете это кэширование, зависит от вас. Из моего опыта у вас должна быть папка для каждого поддерживаемого языка и массив для каждого идентификатора страницы. Кэш должен быть перестроен после обновления перевода. ТОЛЬКО измененный массив должен быть восстановлен.

  4. я думаю, что ответил на это в # 2

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

URL-адреса должны быть переведены с использованием сохраненных слагов в таблице перевода.

Заключительные слова

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

Взгляни на Компонент перевода Symfony. Это может быть хорошей базой кода для вас.

4

Я не собираюсь пытаться уточнить ответы, которые уже даны. Вместо этого я расскажу вам о том, как мой собственный PHP-фреймворк OOP обрабатывает переводы.

Внутренне, мой фреймворк использует такие коды, как en, fr, es, cn и так далее. Массив содержит языки, поддерживаемые веб-сайтом: array (‘en’, ‘fr’, ‘es’, ‘cn’)
Код языка передается через $ _GET (lang = fr), и если он не передан или недействителен, он устанавливается на первый язык в массиве. Таким образом, в любой момент во время выполнения программы и с самого начала, текущий язык известен.

Полезно понять, какой контент нужно переводить в типичном приложении:

1) сообщения об ошибках из классов (или процедурный код)
2) сообщения об ошибках от классов (или процедурный код)
3) содержимое страницы (обычно хранится в базе данных)
4) строки всего сайта (например, название сайта)
5) специфичные для скрипта строки

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

Второй тип сообщений об ошибках больше похож на сообщения, которые вы получаете, когда проверка формы прошла неправильно. («Вы не можете оставить … пустым» или «пожалуйста, выберите пароль длиной более 5 символов»). Строки должны быть загружены до запуска класса. Я знаю, что

Для фактического содержимого страницы я использую одну таблицу на язык, каждая таблица имеет префикс кода для языка. Таким образом, en_content — это таблица с содержанием на английском языке, es_content — для Испании, cn_content — для Китая, а fr_content — для французского.

Четвёртый вид строки актуален на вашем сайте. Это загружается через файл конфигурации, названный с использованием кода для языка, то есть en_lang.php, es_lang.php и так далее. В глобальный языковой файл вам нужно будет загрузить переведенные языки, такие как массив («английский», «китайский», «испанский», «французский»), в глобальный файл и массив «Английский» («Anglais», «Chinois», « Espagnol ‘,’ Francais ‘) во французском файле. Поэтому, когда вы заполняете раскрывающийся список для выбора языка, он отображается на правильном языке;)

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

В моем цикле приложений сначала загружается глобальный языковой файл. Там вы найдете не только глобальные строки (например, «Jack’s Website»), но и настройки для некоторых классов. В основном все, что зависит от языка или культуры. Некоторые из этих строк включают маски для дат (MMDDYYYY или DDMMYYYY) или языковые коды ISO. В основной языковой файл я включаю строки для отдельных классов, потому что их так мало.

Второй и последний языковой файл, который читается с диска, является языковым файлом сценария. lang_en_home_welcome.php — это языковой файл для сценария home / welcome. Сценарий определяется режимом (дом) и действием (приветствие). У каждого скрипта есть своя папка с файлами настроек и языков.

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

Если что-то идет не так, менеджер знает, где взять файл ошибок, зависящий от языка. Этот файл загружается только в случае ошибки.

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

Просто краткое последнее слово о том, как реализованы строки перевода. В моем фреймворке есть один глобальный объект $ manager, который запускает сервисы, доступные для любого другого сервиса. Так, например, служба форм получает службу html и использует ее для написания html. Одной из служб в моей системе является услуга переводчика. $ translationator-> set ($ service, $ code, $ string) устанавливает строку для текущего языка. Языковой файл представляет собой список таких утверждений. $ translationator-> get ($ service, $ code) извлекает строку перевода. Код $ может быть числовым, например 1, или строкой, например, no_connection. Между сервисами не может быть конфликтов, потому что у каждого есть свое пространство имен в области данных переводчика.

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

4
По вопросам рекламы [email protected]