Я уже несколько месяцев бьюсь над этим вопросом, но у меня не было ситуации, в которой мне нужно было бы изучить все возможные варианты раньше. Прямо сейчас я чувствую, что пришло время узнать о возможностях и создать свое личное предпочтение для использования в моих будущих проектах.
Позвольте мне сначала набросать ситуацию, которую я ищу
Я собираюсь обновить / перестроить систему управления контентом, которую я использую уже довольно давно. Тем не менее, я чувствую, что мультиязычность — большое улучшение этой системы. Раньше я не использовал никаких фреймворков, но я собираюсь использовать Laraval4 для предстоящего проекта. Laravel кажется лучшим выбором более чистого способа кодирования PHP. Sidenote: Laraval4 should be no factor in your answer
, Я ищу общие способы перевода, которые не зависят от платформы / фреймворка.
Что должно быть переведено
Поскольку система, которую я ищу, должна быть максимально удобной для пользователя, метод управления переводом должен быть внутри CMS. Не должно быть необходимости устанавливать FTP-соединение для изменения файлов перевода или любых разбираемых шаблонов html / php.
Кроме того, я ищу самый простой способ перевода нескольких таблиц базы данных, возможно, без необходимости создания дополнительных таблиц.
Что я придумала сама
Как я уже искал, читал и пробовал сам. У меня есть несколько вариантов. Но я все еще не чувствую, что достиг наилучшего метода для того, что действительно ищу. Прямо сейчас, это то, что я придумал, но у этого метода также есть побочные эффекты.
Controller.View.parameter
, Таблица базы данных будет иметь эти поля длинной с value
поле. Внутри шаблонов мы могли бы использовать какой-то метод сортировки, такой как echo __('Controller.View.welcome', array('name', 'Joshua'))
и параметр содержит Welcome, :name
, Таким образом, результат Welcome, Joshua
, Это кажется хорошим способом сделать это, потому что такие параметры, как: name, легко понять редактору.languages/en_EN/Controller/View.php
или .ini, что вам больше подходит. Возможно, .ini даже в конце разбирается быстрее. Это должно содержать данные в format parameter=value;
News
а также News_translations
) — вариант, но очень хочется много работать, чтобы получить хорошую систему. Одна из вещей, которые я придумал, основана на data versioning
Система, которую я написал: есть одно имя таблицы базы данных Translations
эта таблица имеет уникальную комбинацию language
, tablename
а также primarykey
, Например: en_En / News / 1 (ссылается на английскую версию новости с идентификатором = 1). Но у этого метода есть два огромных недостатка: во-первых, эта таблица имеет тенденцию получать довольно много времени с большим количеством данных в базе данных, и, во-вторых, использование этой установки для поиска в таблице было бы адской работой. Например. поиск слагаемого SEO для этого элемента будет полнотекстовым поиском, что довольно глупо. Но с другой стороны: это быстрый способ очень быстро создавать переводимый контент в каждой таблице, но я не верю, что этот профессионал перевешивает доводы «против».Итак, они есть. Мои идеи пока. Они даже не включают в себя опции локализации для дат и т. Д., Но, поскольку мой сервер поддерживает 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
:
Я забыл упомянуть: функция __ () является псевдонимом для перевода заданной строки. В этом методе, очевидно, должен быть какой-то резервный метод, в котором текст по умолчанию загружается, когда еще нет доступных переводов. Если перевод отсутствует, он должен быть либо вставлен, либо файл перевода должен быть восстановлен.
В многоязычном сайте есть три различных аспекта:
Хотя все они связаны между собой по-разному, с точки зрения CMS они управляются с использованием различных элементов пользовательского интерфейса и хранятся по-разному. Вы, кажется, уверены в своей реализации и понимании первых двух. Вопрос был о последнем аспекте — «Перевод URL? Должны ли мы делать это или нет? И каким образом?»
Очень важная вещь, не увлекайтесь IDN. Вместо одолжения транслитерация (также: транскрипция и латинизация). Хотя на первый взгляд IDN кажется приемлемым вариантом для международных URL-адресов, на самом деле он не работает так, как рекламируется по двум причинам:
'ч'
или же 'ž'
в '%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']
для этого конкретного браузераВо-первых, вам нужно сопоставить запрос с одним из определенных шаблонов маршрутизации (если вы выбрали 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 простых, приоритетных варианта:
[:language]
сегмент установлен, используйте его$_COOKIE['lang']
установлен, используйте егоЕсли у вас есть язык, вы просто пытаетесь перевести запрос, а если перевод не удался, используйте «значение по умолчанию» для этого конкретного сегмента (на основе результатов маршрутизации).
Да, технически вы можете комбинировать оба подхода, но это усложнит процесс и позволит разместить только людей, которые хотят вручную изменить 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 );
.. или какой-то его вариант, в зависимости от конкретной реализации.
На работе мы недавно реализовали i18n на нескольких наших свойствах, и одной из вещей, с которой мы продолжали бороться, было снижение производительности при работе с переводом на лету, а затем я обнаружил, что это великое сообщение в блоге Томаса Блея что вдохновило то, как мы используем i18n для обработки больших объемов трафика с минимальными проблемами с производительностью.
Вместо вызова функций для каждой операции перевода, которая, как мы знаем в PHP, дорогая, мы определяем наши базовые файлы с помощью заполнителей, а затем используем препроцессор для кэширования этих файлов (мы сохраняем время модификации файла, чтобы убедиться, что мы обслуживаем новейший контент во все времена).
Томас использует {tr}
а также {/tr}
теги, чтобы определить, где перевод начинается и заканчивается. Из-за того, что мы используем TWIG, мы не хотим использовать {
чтобы избежать путаницы, поэтому мы используем [%tr%]
а также [%/tr%]
вместо. В основном это выглядит так:
`return [%tr%]formatted_value[%/tr%];`
Обратите внимание, что Томас предлагает использовать базовый английский в файле. Мы не делаем этого, потому что мы не хотим изменять все файлы перевода, если мы изменим значение на английском языке.
Затем мы создаем файл 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
параметр для переопределения языка, но я не предлагаю субдомен на язык или страницу на язык, потому что это усложнит просмотр популярных страниц и уменьшит ценность входящих ссылок, поскольку они у вас будут более редко распространяется.
Нам нравится этот метод предварительной обработки по трем причинам:
Мы просто добавляем столбец для контента в нашей базе данных под названием language
Затем мы используем метод доступа для LANG
константа, которую мы определили ранее, поэтому наши вызовы SQL (к сожалению, с использованием ZF1) выглядят так:
$query = select()->from($this->_name)
->where('language = ?', User::getLang())
->where('id = ?', $articleId)
->limit(1);
Наши статьи имеют сложный первичный ключ над id
а также language
так статья 54
может существовать на всех языках. наш LANG
по умолчанию en_US
если не указано
Я бы объединил две вещи здесь, одна из них — функция в вашем загрузчике, которая принимает $_GET
параметр для языка и переопределяет переменную cookie, а другой — маршрутизацию, которая принимает несколько слагов. Тогда вы можете сделать что-то вроде этого в своей маршрутизации:
"/wilkommen" => "/welcome/lang/de"... etc ...
Они могут быть сохранены в виде плоского файла, который может быть легко записан с вашей панели администратора. JSON или XML могут обеспечить хорошую структуру для их поддержки.
Перевод на лету на основе PHP
Я не вижу, чтобы они предлагали какое-либо преимущество перед предварительно обработанными переводами.
Front-end основанные переводы
Я давно нашел это интересным, но есть несколько предостережений. Например, вы должны предоставить пользователю полный список фраз на вашем веб-сайте, который вы планируете перевести, это может быть проблематично, если есть области сайта, которые вы скрываете или не разрешили им доступ к ним.
Вы также должны были бы предположить, что все ваши пользователи желают и могут использовать Javascript на вашем сайте, но по моей статистике, около 2,5% наших пользователей работают без него (или используют Noscript, чтобы заблокировать использование наших сайтов) ,
Переводы на основе базы данных
Скорости соединения с базой данных в PHP не являются чем-то особенным, и это увеличивает и без того высокие затраты на вызов функции для каждой фразы для перевода. Производительность & проблемы масштабируемости кажутся непреодолимыми при таком подходе.
Я предлагаю вам не изобретать колесо и использовать список сокращений языков 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, чтобы иметь больше идей. У него достаточно понятная схема БД и достаточно хороший интерфейс для перевода. Как вы создаете статью и выбираете язык для нее. А потом вы можете перевести его на другие языки.
Я думаю, что это не проблема с URL-слизнями. Вы можете просто создать отдельную таблицу для слизней, и это будет правильным решением. Также, используя правильные индексы, нет проблем с запросом таблицы даже с огромным количеством данных.
И это был не полнотекстовый поиск, а совпадение строк, если для slug будет использоваться тип данных varchar, и у вас также может быть индекс для этого поля.
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['language'];
Чтобы упростить его, давайте превратим его в код, который можно легко использовать:
$language=$_COOKIE['language'];
Если ваш язык cookie — валлийский, и у вас есть этот код:
echo $welcome[$language];
Результатом этого будет:
Croeso!
Если вам нужно добавить много переводов для вашего сайта, а база данных слишком трудоемка, использование массива может стать идеальным решением.
Я предлагаю вам не зависеть от базы данных для перевода, это может быть очень сложным делом и может стать серьезной проблемой в случае кодирования данных.
Я столкнулся с подобной проблемой некоторое время назад и написал следующий класс, чтобы решить мою проблему
<?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
заголовокПросто дополнительный ответ:
Абсолютно используйте переведенные URL с идентификатором языка перед ними: http://www.domain.com/nl/over-ons
Гибридные решения, как правило, усложняются, поэтому я просто придерживаюсь этого. Зачем? Потому что URL имеет важное значение для SEO.
О переводе БД: Количество языков более или менее фиксировано? Или скорее непредсказуемый и динамичный? Если это будет исправлено, я просто добавлю новые столбцы, в противном случае использовать несколько таблиц.
Но, в общем, почему бы не использовать Drupal? Я знаю, что каждый хочет создать свою собственную CMS, потому что она быстрее, экономичнее и т. Д. И т. Д. Но это просто плохая идея!
У меня была такая же проблема некоторое время назад, прежде чем начать использовать Symfony фреймворк.
Просто используйте функцию __ (), которая имеет arameters pageId (или objectId, objectTable, описанный в # 2), целевой язык и необязательный параметр резервного языка (по умолчанию). Язык по умолчанию может быть установлен в некоторых глобальных конфигурациях, чтобы иметь более простой способ изменить его позже.
Для хранения контента в базе данных я использовал следующую структуру: (pageId, language, content, variable).
pageId будет FK для вашей страницы, которую вы хотите перевести. если у вас есть другие объекты, такие как новости, галереи или что-то еще, просто разбейте его на 2 поля: objectId, objectTable.
язык — очевидно, он будет хранить строку языка ISO EN_en, LT_lt, EN_us и т. д.
content — текст, который вы хотите перевести вместе с подстановочными знаками для замены переменных. Пример «Здравствуйте, г-н. %% name %%. Баланс вашего счета равен %% balance %%.»
переменные — json-кодированные переменные. PHP предоставляет функции для быстрого их анализа. Пример «имя: Лауринас, баланс: 15.23».
Вы упомянули также слизняк. Вы можете свободно добавить его в эту таблицу просто для быстрого поиска.
Вызовы вашей базы данных должны быть сведены к минимуму с кэшированием переводов. Он должен храниться в массиве PHP, потому что это самая быстрая структура в языке PHP. Как вы сделаете это кэширование, зависит от вас. Из моего опыта у вас должна быть папка для каждого поддерживаемого языка и массив для каждого идентификатора страницы. Кэш должен быть перестроен после обновления перевода. ТОЛЬКО измененный массив должен быть восстановлен.
я думаю, что ответил на это в # 2
ваша идея совершенно логична. этот довольно простой, и я думаю, не доставит вам никаких проблем.
URL-адреса должны быть переведены с использованием сохраненных слагов в таблице перевода.
Заключительные слова
всегда полезно исследовать лучшие практики, но не изобретать велосипед. просто возьмите и используйте компоненты из хорошо известных фреймворков и используйте их.
Взгляни на Компонент перевода Symfony. Это может быть хорошей базой кода для вас.
Я не собираюсь пытаться уточнить ответы, которые уже даны. Вместо этого я расскажу вам о том, как мой собственный 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. Между сервисами не может быть конфликтов, потому что у каждого есть свое пространство имен в области данных переводчика.
Я публикую это здесь в надежде, что это спасет кого-то от необходимости заново изобретать колесо, как я это делал несколько лет назад.