Переклад сайту за допомогою 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:
- _('message')
- _('Message')
- _('Second message')
- _('Message #%d')
- ngettext('message', 'messages', 4)
- 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 для інших проектів з іншими мовами програмування.