Переклад сайту за допомогою gettext

Досить часто перед веб-розробниками виникає необхідність перекласти сайт кількома мовами, зробити сайт мультимовним. Багато веб-підмайстрів використовують для зберігання перекладів базу даних, створюючи на неї додаткове і непотрібне навантаження. Звернення до файлової системи має вищу швидкість, але створює потребу грамотно обмежити зберігання та зчитування перекладів. Благо, у PHP вже є максимально оптимальний для таких цілей механізм gettext, який не просто працює з файловою системою, так ще й з скомпільованими файлами, забезпечуючи найкращу продуктивність.

Функції gettext реалізують NLS (Native Language Support) API. Офіційна документація знаходиться на веб-сайті gnu.org. Для роботи модуля потрібний пакет gettext. Його можна встановити з репозиторіїв, наприклад, у Debian-дистрибутивах це буде так:

sudo apt-get install gettext

Перевірити версію встановленого пакета можна командою:

Застосування

Найпростіший приклад використання gettext у PHP:

echo _ ("message");

Слово "message" шукається у бібліотеці перекладів та, за його наявності, виводиться знайдений переклад, інакше виводиться вихідне слово "message".

Самі переклади зберігати не важливо де, головне щоб були доступні для коду. Для зручності розташуємо їх у корені сайту в папці langs. Нехай буде 3 мови: німецька, англійська та українська. Кінцева структура папки langs буде такою:

. ├──de_DE │ └──LC_MESSAGES │ ├── de_DE.mo │ └── de_DE.po ├──en_US └──ru_RU └──LC_MESSAGES ├── ru_RU.mo └ ── ru_RU.po

Файли *.po містять переклади у текстовому вигляді, *.mo - їх компільовані версії. В імені файлів зручно використовувати їхній локаль. Англійській мовіці файли не обов'язкові, тому що всі ключі в коді писатимемо на ньому. Для ключів можна використовувати будь-яку іншу мову, аби кодування дозволяло, але якщо сайт міжнародний, то вкрай бажано використовувати англійську.

Структура po-файлів проста:

msgid "message" msgstr "повідомлення"

На початку файлу пишуть службову інформацію, наприклад, для української мови можна написати так:

"POT-Creation-Date: 2016-04-10 17:15+0500\n" "PO-Revision-Date: 2016-04-29 02:14+0500\n" "Last-Translator : Sergey\n" "Language: ru_RU\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n % 10==1 && n % 100!=11 ? 0 : n % 10>= 2 &n % 10 =20) ? 1 : 2);\n"

Рядок "Plural-Forms" задає правила для форм численних чисел. Нижче на прикладі її буде розібрано докладніше.

Підключення файлів із перекладами до проекту:

define('BASE_PATH', realpath(dirname(__FILE__))); define ('LANGUAGES_PATH', BASE_PATH.'/langs');

putenv ("LC_ALL=". $locale); setlocale (LC_ALL, $locale, $locale. '.utf8'); bind_textdomain_codeset ( $locale , 'UTF-8' ) ; bindtextdomain ( $locale , LANGUAGES_PATH ) ; textdomain ($ locale);

Тут підключається українська мова, щоб підключити іншу мову, її потрібно так само записати в змінну $locale. Брати його з виставлених користувачем налаштувань або URL-адреси сайту - цей вибір залежить від особливостей Вашого сайту.

Приклад використання у коді:

echo mb_ucfirst (_ ('message')). '. '. _ ('Message'). '. '. _ ( 'Second message'). '. '. sprintf (_ ('Message # % d'), 3). '. 4'. ngettext ( 'message', 'messages', 4). '. 5'.ngettext ( 'message', 'messages', 5). '.' ;

Тут шість звернення до gettext:

  1. _('message')
  2. _('Message')
  3. _('Second message')
  4. _('Message #%d')
  5. ngettext('message', 'messages', 4)
  6. ngettext('message', 'messages', 5)

Звернення 1, 2 та 3 максимально прості: є ключ, шукається переклад. Звертання 4 з цієї позиції нічим не відрізняється від попередніх, різниця лише в подальшому використанні: вставлено %d для підставлення туди чисел, наприклад, за допомогою sprintf. Також можна використовувати будь-які інші ключові слова для їхньої наступної заміни, наприклад "%username%", і замінювати їх потім за допомогою str_replace, головне не перевести їх. Звернення 5 та 6 використовують множинні форми.

Функція mb_ucfirst самописна, бо ucfirst погано працює з юнікодом. У надії що з'явиться нативна функція mb_ucfirst перед визначенням функції робиться перевірка її існування. Іноді доводиться використовувати одне й те саме у різних регістрах, у разі подібні функції допомагають.

if (! function_exists('mb_ucfirst')) function mb_ucfirst($string) mb_substr($string, 1, mb_strlen($string), 'UTF-8'); > >

У результаті файл langs/ru_RU/LC_MESSAGES/ru_RU.po нехай буде таким (дати будуть змінюватися автоматично описаним нижче):

msgid "" msgstr "" "POT-Creation-Date: 2016-04-10 17:15+0500\n" "PO-Revision-Date: 2016-04-29 02:14 +0500\n" "Last-Translator: username\n" "Language: ru_RU\n" "MIME-Version: 1.0\n" "Content-Type: text/plain charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n % 10==1 && n % 100! = 11?0: n%10>=2 && n% 10 = 20)? 1 : 2);\n"

msgid "Second message" msgstr "Друге повідомлення"

msgid "Message #%d" msgstr "Повідомлення #%d"

msgid "message" msgid_plural "messages" msgstr[0] "повідомлення" msgstr[1] "повідомлення" msgstr[2] "повідомлень"

Третій запис ("message") покриває звернення 1, 5 і 6, перший і другий - 3 і 4. Переклад звернення 2 не буде знайдено, т.к. реєстрозалежність.

Третій запис тут найцікавіший, т.к. описує множинні форми. Правила описані на початку файлу у пункті "Plural-Forms".

nplurals - кількість форм, у разі дорівнює трьом. plural - самі правила, являють собою вкладений тернарний оператор, який повертає число від 0 до 2, залежно від цього числа береться елемент з msgstr. Порівнявши ці умови та числа з прикладом з po-файлу, все стане зрозуміло. Правила у різних мовах відрізняються.

Німецький файл langs/de_DE/LC_MESSAGES/de_DE.po заповнюється за аналогією:

msgid "" msgstr "" "POT-Creation-Date: 2016-04-10 17:15+0500\n" "PO-Revision-Date: 2016-04-29 02:14 +0500\n" "Last-Translator: username\n" "Language: de_DE\n" "MIME-Version: 1.0\n" "Content-Type: text/plain charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"

msgid "Second message" msgstr "Zweite nachricht"

msgid "Message #%d" msgstr "Nachricht #%d"

msgid "message" msgid_plural "messages" msgstr[0] "nachricht" msgstr[1] "nachrichten"

У ньому багатьох форм дві, тому й правило помітно простіше. Також змінився пункт "Language". Це єдині поля в даному прикладі, які потрібно змінювати при додаванні нових мов.

Компілювання MO

З po-файлів отриматискомпільований mo-файл можна декількома способами.

Перший – за допомогою програм на кшталт Poedit. У ній є редагування po-файлів і можливість автоматично компілювати файл MO при збереженні. Вмикається/вимикається вона у загальних налаштуваннях програми. Мінус – не можна задати параметри цього компілювання. Плюс – там чимало можливостей для редагування перекладів; наприклад, сканування коду на предмет виклику gettext і додавання їх до PO.

Другий спосіб – консольна команда msgfmt. Повний список параметрів доступний у документації за посиланням, але зазвичай досить проста команда:

/usr/bin/msgfmt "ru_RU.po" -f -o "ru_RU.mo"

Тут файл ru_RU.mo компілюється з вихідних файлів ru_RU.po з використанням fuzzy-записів. Fuzzy – це нечіткі переклади, які можуть бути некоректними. З'являються вони, наприклад, під час автоматичних перекладів. Якщо виникають сумніви щодо правильності цих перекладів, то параметр -f слід прибрати.

Щоб скомпілювати всі файли в папці langs, можна скористатися bash-скриптом. Припустимо, що всі bash-скрипти розташовані в папці bash в корені сайту:

cd ../langs for lang_locale in *; do if [ ! -f "$lang_locale /LC_MESSAGES/ $lang_locale .po"]; then continue fi cd " $lang_locale /LC_MESSAGES" / usr / bin / msgfmt " $lang_locale .po" -f -o " $lang_locale .mo" echo " $ lang_locale - compiled" cd .. / .. / done

Перевірку наявності файлу (третій рядок) не можна назвати обов'язковою, але вона запобігатиме помилкам у разі якщо в папці знайдуться якісь файли та папки, що не відносяться до перекладів. Підсумок виконання:

$sh compile.sh de_DE - compiled ru_RU - compiled

Після цього можна перевірити виведення перекладів через PHP. Підсумковийопис index.php:

define ( 'BASE_PATH' , realpath ( dirname ( __FILE__ ) ) ) ; define ( 'LANGUAGES_PATH' , BASE_PATH . '/langs' ) ;

putenv ( 'LC_ALL=' . $locale ) ; locale(LC_ALL, $locale, $locale.'.utf8'); bind_textdomain_codeset($locale, 'UTF-8'); bindtextdomain($locale, LANGUAGE_PATH); textdomain($locale);

if ( ! function_exists ( 'mb_ucfirst ' ) ) < функція mb_ucfirst($string) < повернути mb_strtoupper(mb_substr($string, 0, 1, 'UTF-8'), 'UTF-8'). mb_substr($string, 1, mb_strlen($string), 'UTF-8'); > >

echo mb_ucfirst ( _ ('повідомлення')) . '. '. _ ( 'Повідомлення' ) . '. '. _ ('Друге повідомлення') . '. '. sprintf(_('Повідомлення #%d'), 3). '. 4 '. ngettext('повідомлення', 'повідомлення', 4). '. 5 '. ngettext('повідомлення', 'повідомлення', 5). '.' ;

Підпишіться на цю сторінку:

повідомлення. Повідомлення. Второе сообщение. Огляд №3. 4 роки тому. 5 років тому.

Отримуйте останні оновлення діапазону de_DE.

Якщо висновок по-прежнему на англійському, то, ймовірно, у Вашій системі встановлені потрібні місця. Сторінки Інший бренд Веб-сайт Особистий блог:

Деталі наступного:

sudo locale-gen of_DE.utf8

Після добалення нової локації іноді потрібна апача, інакше вона її не побачить.

перезапуск служби sudo apache2

Сканування коду, додавання нових перекладів

Ручное добвлення нової фрази у файли у врученні вазрботки - не самий у добний спосіб, із-за нього приходить від вашої сазрботки. Якщо ви хочете отримати це, ви можете отримати це - сніжинки ся тоді він і виведеться. В такому випадкузнадобиться сканування коду щодо нових перекладів. Для цього використовується утиліта xgettext.

Додамо в index.php нове слово, нехай буде "Application":

echo mb_ucfirst (_ ('message')). '. '. _ ('Message'). '. '. _ ( 'Second message'). '. '. sprintf (_ ('Message # % d'), 3). '. 4'. ngettext ( 'message', 'messages', 4). '. 5'. ngettext ( 'message', 'messages', 5). '. '; echo _ ("Application");

Bash-скрипт для сканування коду та додавання нових знайдених перекладів нехай лежить у тій самій папці bash. Спочатку розберемо цей приклад сканування:

LIST = `find. -name "*.php" ` / usr / bin / xgettext --language = PHP $ LIST --from-code = UTF-8 --no-location --no-wrap -o / tmp / xgettext. pot

Файл, що вийшов, - це стандартний po-файл, тільки без налаштувань і перекладів. У ньому знаходяться всі фрази, що використовуються в index.php: до трьох вже наявних в наших файлах додалися дві - "Message" і "Application". Додавати їх вручну до наявних теж не варіант, для цих цілей краще підійде утиліта msgmerge. У результаті зі скануванням вийде такий bash-скрипт:

cd .. echo "Parsing. " LIST = ` find . -name "*.php" ` / usr / bin / xgettext --language = PHP $ LIST --from-code = UTF-8 --no-location --no-wrap -o / tmp / xgettext. pot

echo "Merging." cd langs

for lang_locale in *; do if [ ! -f "$lang_locale /LC_MESSAGES/ $lang_locale .po"]; then continue fi echo $lang_locale cd " $lang_locale /LC_MESSAGES" / usr / bin / msgmerge " $lang_locale .po" / tmp / xgettext.pot -U -- backup =off --no-wrap --no-fuzzy-matching cd .. / .. / done

Злиття файлів $lang_locale.po та xgettext.pot відбувається з оновленням першого (параметр -U), без створеннябэкапа (--backup=off; бо навіщо воно коли є git і аналоги), без переносів довгих рядків (--no-wrap; бо це заважає читання вихідних слів) і без автоперекладу за наявними фразами (--no-fuzzy-matching; дивний включений за умовчанням функціонал, який шукає за наявними перекладами схожі і додає їх до нових фраз з позначкою fuzzy, при цьому працює погано і трохи сповільнює весь процес злиття).

$ sh lang.sh Parsing. Merging. de_DE . завершено. ua_UA . завершено.

Кількість точок перед "завершено" залежить від часу виконання.

Після виконання команди обидва відсутні слова додалися в po-файли всередині папки langs. Тепер їм варто додати переклад (вручну або за допомогою Poedit), а потім виконати компілювання.

Кінцеве дерево файлів даного проекту виглядає так:

. ├──bash │ ├── compile.sh │ └── lang.sh ├── index.php └──langs ├──de_DE │ └──LC_MESSAGES │ ├── de_DE.mo │ └── de_DE.po ├──en_US └──ru_RU └──LC_MESSAGES ├── ru_RU. mo └── ru_RU.po

Додавання нової мови

Наприклад, потрібно додати іспанську мову. Позначення локалі мови можна знайти, наприклад, тут або тут (але замість "-" використовувати "_"). Міжнародній іспанській (або іспанській з Іспанії) відповідає позначення es_ES.

Потрібно зробити ці кроки:

Потім, якщо сайт вже робочий, якось додати нову мову в інтерфейс, щоб користувачі могли його вибрати і використовувати.

Аналогічно можна використовувати gettext для інших проектів з іншими мовами програмування.