graphics.hatenablog.com

技術系テクニカルアーティストのあれこれ

PySide で多言語対応してみる。

f:id:hal1932:20190813202035p:plain f:id:hal1932:20190813202052p:plain
maya_test/main_window.py at master · hal1932/maya_test · GitHub

そのうち必要になる気がするので手順をメモっておく。

Qt Linguist のインストール

普段 Qt Designer とかを使わない*1人なので各種バイナリを一切インストールしておらず。。仕方ないので Qt の SDK をいれることに。とりあえずどのビルドでもいいからインストールすれば Linguist も一緒についてくる。

多言語対応の UI を組む

まず最初に、Linguist に対応できるように UI を組んでやる必要がある。Qt Designer を使う場合は何も考えなくていいんだけど、コードで書くときは QObject.tr() 経由で文字列を設定する*2必要がある。

class QObject(__Shiboken.Object):
    def tr(self, *args, **kwargs): # real signature unknown
        pass
class MainWindow(QMainWindow, MayaQWidgetBaseMixin):
    def __init__(self):
        ...  # 諸々初期化
        label1 = QLabel(self.tr('label1'))
        layout.setWidget(label1)

pyside-lupdate

コードができたら、.pro ファイル*3をこんなかんじで用意する。

# 多言語対応の対象にするソースコード
SOURCES = ..\main_window.py

# 対応言語
TRANSLATIONS = ja_JP.ts en_US.ts

UI を定義してるソースコードPython インストールディレクトリにある Scripts/pyside2-lupdate *4 に渡してやる。例えば Windows ならこんなかんじでバッチ化すればいい。
ここでは Python37 の pip でインストールしたやつを使ってるけど、PySide のバージョンさえ合わせてあれば問題なし。

pushd %~dp0
    C:\Python37\Scripts\pyside2-lupdate.exe i18n_test_app.pro
popd
pause

ここまでやると言語ごとに .ts ファイルが生成されるので、Liguist で読み込んで編集してやればいい。終わったら Linguist の「リリース」メニューから .ts のビルドして、言語ごとに .qm ファイルが生成されれば完了。
lupdate は UI の中にある tr() や translate() を検出して、そこに記述されてる情報を拾ってくるようになってる。既に .ts ファイルがある場合は差分を上書き更新してくれるから、気軽に叩けばいい。

言語の反映

.qm ファイルを QTranslator.load してから、QApplication.installTranslator するだけ。Maya 上から実行する場合はカレントディレクトリが Maya.exe の場所になってるので、load() のオプションに directory を指定するのを忘れないこと。*5

app = QApplication.instance()
translator = QTranslator()
translator.load('ja_JP', directory=os.path.join(os.path.dirname(__file__), 'i18n'))  # ./i18n/ja_JP.qm が存在する想定
app.installTranslator(translator)

動的な言語切り替え

QApplication.installTranslator() に従って QObject.tr() の返り値が変わるので、動的に切り替えたい場合は installTranslator 後に tr が再度呼び出されるように組んでやればいい。pyside-uic を使っている場合は retranslateUi() が自動生成されてるので、installTranslator の直後にそれを叩けばいい。

UI をコードで書いてる場合も同じで、例えば簡易的な実装だけどこんなのでも別に構わない。*6

class MayaMainWindowBase(QMainWindow, MayaQWidgetBaseMixin):
    def setup_ui(self):
        # type: () -> MayaMainWindowBase
        widget = self.centralWidget()
        if widget is not None:
            widget.deleteLater()
        widget = QWidget()

        self.setCentralWidget(widget)
        self._setup_ui(widget)
        return self

    def _setup_ui(self, central_widget):
        # type: (QWidget) -> NoReturn
        pass

class MayaAppBase(object):
    def execute(self):
        app = QApplication.instance()
        self._initialize(app)
        self._window = self._create_window()
        if self._window is not None:
            self._window.setup_ui().show()
class MainWindow(MayaMainWindowBase):
    lang_switch_requested = Signal(str)

    def _setup_ui(self, central_widget):
        # type: (QWidget) -> NoReturn
        layout = QVBoxLayout()
        switch_button = QPushButton(self.tr('switch_lang'))
        switch_button.clicked.connect(lambda: self.lang_switch_requested('ja_JP'))
        layout.addWidget(switch_button)
        central_widget.setLayout(layout)
class MayaApp(MayaAppBase):
    def __init__(self):
        super(MayaApp, self).__init__()
        self.__translator = QTranslator()

    def _initialize(self, app):
        # type: (QApplication) -> NoReturn
        self.__switch_languages('ja_JP')
        app.installTranslator(self.__translator)

    def _create_window(self):
        # type: () -> MayaMainWindowBase
        window = MainWindow()
        window.lang_switch_requested.connect(lambda lang: self.__switch_languages(lang))
        return window

    def __switch_languages(self, lang_name):
        # type: (str) -> NoReturn
        self.__translator.load(lang_name, directory=os.path.join(os.path.dirname(__file__), 'i18n'))

*1:このへんは人によって色々な考え方があると思うので諸々割愛。

*2:QObject.tr() 自体は QObject.translate() のラッパーとして実装されてる。translate を使ってもいいんだけど、PySide で組む程度の UI なら tr が使えれば十分かな。

*3:Qt Creator 用のプロジェクトファイル。PySide の場合は Creator を使わないので手書きする。

*4:PySide の場合は pyside-lupdate

*5:ちょっと話はそれるけど……、そもそもの話として余程の強い仮定でもない限りは "暗黙のカレントディレクトリ" なんてものを信用してはいけない。実行コンテキストを確実に掌握すること。

*6:実戦投入するならこんな雑なやり方はせずに、自動で setText() しなおしてくれる仕組みをちゃんと設計すべき。