目次

i18n - Twig

i18n = i(18文字)n = internationalization

サイトを国際化対応すること。つまり、ページを見る人によって、言語や時刻や通貨などを変える。

言語の国際化に便利な機能として、gettextというライブラリがC言語で開発されており、PHPでも拡張として利用できる。

そのPHP用gettextを、さらにTwig側で拡張してTwigの記法で埋め込めるようにしたものを利用する。

流れ

  1. 環境を用意する(PHP用gettext拡張、Twig用i18n拡張、OS言語設定)
  2. Twigファイル中の翻訳したい文言が書かれた部分を「{% trans %} {% endtrans %}」で囲う
  3. Twigファイルをphpファイルに変換する
  4. phpから翻訳が必要な箇所(transで囲んだ箇所)を自動抽出した.poファイルを作る
  5. .poファイル内の文言を1つ1つ別の言語に翻訳する
  6. .poファイルを.moファイルにコンパイルする
  7. .moファイルを所定のフォルダ構造に配置し、Twigにパスを与える
  8. 準備完了!
  1. アクセスが来たら、ブラウザ情報などを読み取ってOSの言語を(PHP上で仮想的に)変更する
  2. 翻訳された結果が出力される!

導入

gettextのインストール

まず、PHPにgettext拡張が無ければ始まらないのでインストール。といっても勝手に入ってるかも知れないので、phpinfo()等で確認してからでもいいかも。

Twig拡張セットのインストール

Twig用拡張セットをComposerでインストール。i18n拡張もこれに含まれている。Twigのバージョンが古すぎると失敗する(1.27以降?)

> composer require twig/extensions

OSの言語の確認、インストール

gettextはOSの言語設定を読み取って設定される。PHPで利用する際は、setlocale()で目的の言語に変更する。

その際、OSにインストールされている言語でないと変更できない。(言語設定は実質的な処理には全く関わってこないため、無駄な気もするが)目的の言語が入っているか確認し、無ければインストールする。

$ locale -a
C
C.UTF-8
en_US.utf8
...

ここで、ja_JPが欲しいのに無かったとする。

(Ubuntuの場合)
$ sudo apt-get install language-pack-ja

これで入る。

$ locale -a
(略)
en_US.utf8
ja_JP.utf8

このlocaleコマンドで出てきた言語名は、後で利用するので控えておく。

一度apacheを再起動する。

$ sudo service apache2 restart
裏技?

以下のページで、OSの言語を変更せずともgettextで言語を切り替えようと試みられている。

$domainの部分に言語名を指定することで、OSの言語をそのままにしつつ、.poのファイル名による言語切り替えが可能になるという方法らしい。

が、どうもOSの言語の設定が「C」の場合は、gettextは翻訳を行わずそのままの文字列を出力するため、Ubuntuなどはダメなようだ。

使い方

例えばこんなシステム構成とする

root/
|- app
|   |- templates              Twig用テンプレート
|   |   `- index.twig
|   |- templates_cache        Twig用キャッシュ
|   `- locale                 gettext用翻訳ファイル
|       |- en_US
|       |   `-LC_MESSAGES
|       |      |- messages.po (まだ無くてよい)
|       |      `- messages.mo (まだ無くてよい)
|       `- ja_JP
|           `-LC_MESSAGES
|              |- messages.po (まだ無くてよい)
|              `- messages.mo (まだ無くてよい)
`- index.php
フォルダ・ファイル命名制限
locale何でもよい
en_US, ja_JP翻訳言語のOS上での名称
LC_MESSAGES固定
messages.po.moの作成に必要なだけで、実際は不要
messages.mo何でもよいが、元となる翻訳文言が同じファイルは各言語で名称を統一する

インスタンス生成時に拡張を指定

<?php
require_once '/path/to/vendor/autoload.php';

$loader = new Twig_Loader_Filesystem('/path/to/templates');
$twig = new Twig_Environment($loader, ['cache' => '/path/to/compilation_cache']);

// 拡張を加える
$twig->addExtension(new \Twig_Extensions_Extension_I18n());

環境設定

PHPよりOSの環境を変更する。

// 目的の言語
$lang = 'ja_JP.utf8';  // localeコマンドで出てきた名称を指定

// ファイル名
// システム構成のLC_MESSAGESフォルダ内にある.poのファイル名と同じものを指定する
$domain = 'messages';
textdomain($domain);

// 言語を指定
setlocale(LC_MESSAGES, $lang);

// localeフォルダまでのパスを指定
bindtextdomain($domain, '/path/to/root/app/locale/');

// 文字コードは適宜指定
bind_textdomain_codeset($domain, 'UTF-8');

.twigファイルにタグを埋め込む

直接書かれた文字列なら、{% trans %}~{% endtrans %}で囲う。{% Twigタグ %}内の文字列なら、「|trans」フィルターを用いる。

<title>English Site!</title>

{% set COMMENT = 'Hello World!' %}

<title>{% trans %}English Site!{% endtrans %}</title>

{% set COMMENT = 'Hello World!'|trans %}

レンダリングしてキャッシュファイルを作る

gettextに付属する、翻訳が必要な箇所を探してリスト化するツールを用いる。あくまでTwigではなくPHP用なので、まず全TwigファイルをレンダリングしてPHPにする。

適当に作業用ディレクトリを用意してRecursiveDirectoryIteratorで全ファイルをTwigに処理させる。

(もしくは、Twigファイル数が知れているなら、自分で1つ1つアクセスして表示させてもよい)

$tplDir = 'app/templates';
$tmpDir = 'app/templates_cache/';
$loader = new Twig_Loader_Filesystem($tplDir);

// force auto-reload to always have the latest version of the template
$twig = new Twig_Environment($loader, array(
    'cache' => $tmpDir,
    'auto_reload' => true
));
$twig->addExtension(new Twig_Extensions_Extension_I18n());
// configure Twig the way you want

// iterate over all your templates
foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($tplDir), RecursiveIteratorIterator::LEAVES_ONLY) as $file)
{
    // force compilation
    if ($file->isFile()) {
        $twig->loadTemplate(str_replace($tplDir.'/', '', $file));
    }
}

キャッシュファイルに対してxgettextで.poファイルを作る

xgettextで、PHPファイル内のgettextを用いると指定した箇所を全て洗い出し、poファイルを作成する。

xgettext --default-domain=messages -p ./app/locale --from-code=UTF-8 -n --omit-hader -L PHP ./app/templates_cache/*/*.php

これで ./app/localemessages.po という名称で、{% trans %}で囲った部分が網羅されたファイルが出来る。(作る場所はどこでもいい)

messages.po
#: /var/www/html/app/template_cache/34/34de2683ab8f366dd6d96d37dfe3fe7057abf8afbedb8074bacdbac0c5cd760a.php:25
msgid "English Site!"
msgstr ""

msgid "Hello World!"
msgstr ""
...

.poファイルを各言語のLC_MESSAGESにコピーして翻訳する

root/app/locale/ja_JP/LC_MESSAGES/messages.po
#: /var/www/html/app/template_cache/34/34de2683ab8f366dd6d96d37dfe3fe7057abf8afbedb8074bacdbac0c5cd760a.php:25
msgid "English Site!"
msgstr "日本語のサイトです!"

msgid "Hello World!"
msgstr "こんにちは西園寺!"

.poファイルを.moファイルにコンパイルする

msgfmtコマンドを使用する。各poファイルのある場所まで行き、

$ msgfmt messages.po -o messages.mo

これで、同じ階層にmoファイルが出来る。ひとまずファイル側の準備は完了。

PHPで、OS言語を変更する処理を実装する

OSの言語を翻訳したいものにすると、gettextが自動的に翻訳結果を作ってくれる。

そのためには、何らかの手段でユーザの求める言語を特定する必要がある。主には以下の3つかな?

ブラウザの言語を最優先にすると、日本語ブラウザで一時的に英語サイトが見たい時とか困るから、GETの判定はそれより前に入れるべき。あとはセッションなどで保持するとよいだろう。

ブラウザの言語判定法

ブラウザでは、言語を優先度付きで複数設定できる。サイトで対応する言語の中から優先順位の高いものを見つけるサンプルコード。

/**
 * @return str 言語のOS上での名称
 */
function judgeLang()
{
	// 対応言語(省略2文字 => OS上での名称)
	$available = ['en' => 'en_US.utf8', 'ja' => 'ja_JP.utf8'];
	$default = 'ja_JP.utf8';

	// ブラウザの言語を取得
	$languages = explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']);
	
	// 頭2文字が候補に無いか確認
	foreach ($languages as $language) {
	    foreach (array_keys($available) as $lk) {
	        $pat = "/^{$lk}/i";
	        if (preg_match($pat, $language)) {
	            $lang = $available[$lk];
	            return $lang;
	        }
	    }
	}
	
	return $default;
}

OS言語の指定方法

judgeLang()から返ってきた言語を設定する。$domainはファイル名。

bindtextdomainにはlocaleフォルダへのパスを指定する。locale/(言語)/LC_MESSAGES/以下のフォルダが探される。

function setLocale($locale)
{
    $domain = 'messages';
    $res = setlocale(LC_MESSAGES, $locale);  // $resには成功フラグが返る
    bindtextdomain($domain, '/path/to/locale/');
    bind_textdomain_codeset($domain, 'UTF-8');
    textdomain($domain);
}

表示確認する

poファイルの更新

これまでの翻訳結果を残しつつ、新規追加された翻訳箇所をpoファイルに加えたい場合。

xgettextは、全ての{% trans %}指定した必要箇所を拾ってくれるのはよいが、作られるのはまっさらなファイルである。

古いpoファイルを残しつつ、まっさらなpotファイルをxgettextで作成し、msgmergeでマージするという方法を採る。

ただ、日本語を含むファイルを読む際、ヘッダに文字コードがUTF-8である旨を明記しないと、「illegal multibyte sequence」エラーで読まれない。

ヘッダのある.poファイルの作り方はこちら、というか上記の作り方ではわざわざomitしてたね。

【.potファイルを作る】
$ xgettext -o {out_path} app/templates_cache/*/*.php
【ja.poを作る】
$ msginit --locale=ja --input={new.po}

ここで出来たja.poだが、ヘッダの文字コードが'ASCII'になってしまっているため、'UTF-8'に書き換える。

# Japanese translations for xxx package.
# Copyright (C) 2018 THE xxx'S COPYRIGHT HOLDER
# This file is distributed under the same license as the xxx package.
#  <vagrant@scotchbox>, 2018.
#
msgid ""
msgstr ""
"Project-Id-Version: xxx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-11-09 12:06+0000\n"
"PO-Revision-Date: 2018-11-09 12:07+0000\n"
"Last-Translator:  <vagrant@scotchbox>\n"
"Language-Team: Japanese\n"
"Language: ja\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"  ←ここ
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"

これを、ヘッダが無い既存の.poの冒頭に記述すると、正しく読まれ、マージされるようになる。

古いファイルも新しいファイルも文字コードを指定したら、マージする。

$ msgmerge {old.po} {new.po} -o {out.po}

マージした.poの抜けている部分を埋め、改めて.moを作成する。

$ msgfmt messages.po -o messages.mo

条件がよくわからないが、上手く更新が反映されないことがあるので、その場合サーバシステムを再起動する。

$ sudo service apache2 restart