はじめてのBlenderアドオン開発

Last Update: 2023.3.1

Blender 2.7

はじめてのBlenderアドオン開発

Blender 2.7

Last Update: 2023.3.1

3-7. アドオンのUIを複数の言語に対応する

BlenderのUIは英語がデフォルトであることから、海外でも多くの3DCGデザイナーがBlenderを使ってCGを作成しています。 このため、アドオンのUIも英語ベースで構築するほうが、アドオンを使ってくれる人が多くなります。 しかし一方で、英語が苦手な方が困らないように、日本語などの英語以外の言語をサポートしたいと思う方もいると思います。 そこで本節では、アドオンのUIを複数の言語に対応する方法を説明します。

作成するアドオンの仕様

本節ではアドオンのUIを複数の言語に対応させる方法について説明するため、新たな機能は作らずに前に紹介したサンプルを改造します。 このため、本節では次のような仕様のアドオンを作成します。

アドオンを作成する

1-5節 を参考にして以下のソースコードを入力し、ファイル名 sample_3_7.py として保存してください。

import bpy
import bmesh
from bpy.props import IntProperty, BoolProperty, PointerProperty


bl_info = {
    "name": "サンプル3-7: マウスの右クリックで面を削除する(多言語対応版)",
    "author": "Nutti",
    "version": (2, 0),
    "blender": (2, 75, 0),
    "location": "3Dビュー > プロパティパネル > マウスの右クリックで面を削除",
    "description": "マウスの右クリックで面を削除するアドオン",
    "warning": "",
    "support": "TESTING",
    "wiki_url": "",
    "tracker_url": "",
    "category": "Mesh"
}


# 翻訳辞書
translation_dict = {
    "en_US": {
        ("*", "Delete Face By Right Click"):
            "Delete Face By Right Click",
        ("*", "Sample3-7: Out of range"):
            "Sample3-7: Out of range",
        ("*", "Sample3-7: No face is selected"):
            "Sample3-7: No face is selected",
        ("*", "Sample3-7: Deleted Face"):
            "Sample3-7: Deleted Face",
        ("*", "Sample3-7: Start deleting faces"):
            "Sample3-7: Start deleting faces",
        ("*", "Sample3-7: %d face(s) are deleted"):
            "Sample3-7: %d face(s) are deleted",
        ("*", "Start"):
            "Start",
        ("*", "End"):
            "End",
        ("*", "Sample3-7: Enabled add-on 'Sample3-7'"):
            "Sample3-7: Enabled add-on 'Sample3-7'",
        ("*", "Sample3-7: Disabled add-on 'Sample3-7'"):
            "Sample3-7: Disabled add-on 'Sample3-7'"
    },
    "ja_JP": {
        ("*", "Delete Face By Right Click"):
            "マウスの右クリックで面を削除",
        ("*", "Sample3-7: Out of range"):
            "サンプル3-7: 選択範囲外です。",
        ("*", "Sample3-7: No face is selected"):
            "サンプル3-7: 面以外を選択しました。",
        ("*", "Sample3-7: Deleted Face"):
            "サンプル3-7: 面を削除しました。",
        ("*", "Sample3-7: Start deleting faces"):
            "サンプル3-7: 削除処理を開始しました。",
        ("*", "Sample3-7: %d face(s) are deleted"):
            "サンプル3-7: %d個の面を削除しました。",
        ("*", "Start"):
            "開始",
        ("*", "End"):
            "終了",
        ("*", "Sample3-7: Enabled add-on 'Sample3-7'"):
            "サンプル3-7: アドオン「サンプル3-7」が有効化されました。",
        ("*", "Sample3-7: Disabled add-on 'Sample3-7'"):
            "サンプル3-7: アドオン「サンプル3-7」が無効化されました。"
    }
}


# プロパティ
class DFRC_Properties(bpy.types.PropertyGroup):

    running = BoolProperty(
        name="動作中",
        description="削除処理が動作中か?",
        default=False
    )
    right_mouse_down = BoolProperty(
        name="右クリックされた状態",
        description="右クリックされた状態か?",
        default=False
    )
    deleted = BoolProperty(
        name="面が削除された状態",
        description="面が削除された状態か?",
        default=False
    )
    deleted_count = IntProperty(
        name="削除した面数",
        description="削除した面の数",
        default=0
    )


# マウスの右クリックで面を削除
class DeleteFaceByRClick(bpy.types.Operator):

    bl_idname = "mesh.delete_face_by_rclick"
    bl_label = bpy.app.translations.pgettext("Delete Face By Right Click")
    bl_description = bpy.app.translations.pgettext(
        "Delete Face By Right Click"
    )

    def modal(self, context, event):
        props = context.scene.dfrc_props

        # 3Dビューの画面を更新
        if context.area:
            context.area.tag_redraw()

        # 起動していない場合は終了
        if props.running is False:
            return {'FINISHED'}

        # クリック状態を更新
        if event.type == 'RIGHTMOUSE':
            if event.value == 'PRESS':
                props.right_mouse_down = True
            elif event.value == 'RELEASE':
                props.right_mouse_down = False

        # 右クリックされた面を削除
        if props.right_mouse_down is True and props.deleted is False:
            # bmeshの構築
            obj = context.edit_object
            me = obj.data
            bm = bmesh.from_edit_mesh(me)
            # クリックされた面を選択
            loc = event.mouse_region_x, event.mouse_region_y
            ret = bpy.ops.view3d.select(location=loc)
            if ret == {'PASS_THROUGH'}:
                print(bpy.app.translations.pgettext("Sample3-7: Out of range"))
                return {'PASS_THROUGH'}
            # 選択面を取得
            e = bm.select_history[-1]
            if not isinstance(e, bmesh.types.BMFace):
                bm.select_history.remove(e)
                print(
                    bpy.app.translations.pgettext(
                        "Sample3-7: No face is selected"
                    )
                )
                return {'PASS_THROUGH'}
            # 選択面を削除
            bm.select_history.remove(e)
            bmesh.ops.delete(bm, geom=[e], context=5)
            # bmeshの更新
            bmesh.update_edit_mesh(me, True)
            # 削除面数をカウントアップ
            props.deleted_count = props.deleted_count + 1
            # マウスクリック中に連続して面が削除されることを防ぐ
            props.deleted = True
            print(bpy.app.translations.pgettext("Sample3-7: Deleted Face"))

        # マウスがクリック状態から解除された時に、削除禁止状態を解除
        if props.right_mouse_down is False:
            props.deleted = False

        return {'PASS_THROUGH'}

    def invoke(self, context, event):
        props = context.scene.dfrc_props
        if context.area.type == 'VIEW_3D':
            # 開始ボタンが押された時の処理
            if props.running is False:
                props.running = True
                props.deleted = False
                props.right_mouse_down = False
                props.deleted_count = 0
                # modal処理クラスを追加
                context.window_manager.modal_handler_add(self)
                print(
                    bpy.app.translations.pgettext(
                        "Sample3-7: Start deleting faces"
                    )
                )
                return {'RUNNING_MODAL'}
            # 終了ボタンが押された時の処理
            else:
                props.running = False
                self.report(
                    {'INFO'},
                    bpy.app.translations.pgettext_iface(
                        "Sample3-7: %d face(s) are deleted"
                    )
                    % (props.deleted_count)
                )
                print(
                    bpy.app.translations.pgettext_iface(
                        "Sample3-7: %d face(s) are deleted"
                    )
                    % (props.deleted_count)
                )
                return {'FINISHED'}
        else:
            return {'CANCELLED'}


# UI
class OBJECT_PT_DFRC(bpy.types.Panel):

    bl_label = bpy.app.translations.pgettext("Delete Face By Right Click")
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"

    def draw(self, context):
        layout = self.layout
        props = context.scene.dfrc_props
        # 開始/停止ボタンを追加
        if props.running is False:
            layout.operator(
                DeleteFaceByRClick.bl_idname,
                text=bpy.app.translations.pgettext("Start"),
                icon="PLAY"
            )
        else:
            layout.operator(
                DeleteFaceByRClick.bl_idname,
                text=bpy.app.translations.pgettext("End"),
                icon="PAUSE"
            )


def register():
    bpy.utils.register_module(__name__)
    sc = bpy.types.Scene
    sc.dfrc_props = PointerProperty(
        name="プロパティ",
        description="本アドオンで利用するプロパティ一覧",
        type=DFRC_Properties
    )
    # 翻訳辞書の登録
    bpy.app.translations.register(__name__, translation_dict)
    print(
        bpy.app.translations.pgettext(
            "Sample3-7: Enabled add-on 'Sample3-7'"
        )
    )


def unregister():
    # 翻訳辞書の登録解除
    bpy.app.translations.unregister(__name__)
    del bpy.types.Scene.dfrc_props
    bpy.utils.unregister_module(__name__)
    print(
        bpy.app.translations.pgettext(
            "Sample3-7: Disabled add-on 'Sample3-7'"
        )
    )


if __name__ == "__main__":
    register()

アドオンを使用する

アドオンを有効化する

1-5節 を参考に作成したアドオンを有効化すると、コンソールウィンドウに文字列が出力されます。 言語を日本語にした場合には、以下の文字列が出力されます。

サンプル3-7: アドオン「サンプル3-7」が有効化されました。

一方、言語を英語にした場合は以下の文字列が出力されます。

Sample3-7: Enabled add-on 'Sample3-7'

アドオンの機能を使用する

有効化したアドオンの機能を使い、動作を確認します。 3-1節 と同じ機能を持つアドオンであるため、本節ではUIの言語を変えたときの動作確認方法の手順のみ説明します。

1 [情報] エリアのメニューから [ファイル] > [ユーザー設定...] を実行し、[システム] タブをクリックします。
2 [ローカライズ] のチェックボックスにチェックが入っていることを確認し、言語を [日本語 (Japanese)] に設定します。[翻訳] ラベルに配置されているボタンが3つとも選択状態であることを確認します。
3 3-1節 に従ってアドオンを使用し、UIやコンソールウィンドウなどに出力されるメッセージが日本語になっていることを確認します。
4 手順1を行った後、[ローカライズ] のチェックボックスにチェックが入っていることを確認し、言語を [英語 (English)] に設定します。[翻訳] ラベルに配置されているボタンが3つとも選択状態であることを確認します。
5 3-1節 に従ってアドオンを使用し、UIやコンソールウィンドウなどに出力されるメッセージが英語になっていることを確認します。

アドオンを無効化する

1-5節 を参考にして有効化したアドオンを無効化すると、コンソールウィンドウに文字列が出力されます。 言語を日本語にした場合には、以下の文字列が出力されます。

サンプル3-7: アドオン「サンプル3-7」が無効化されました。

一方、言語を英語にした場合には以下の文字列が出力されます。

Sample3-7: Disabled add-on 'Sample3-7'

ソースコードの解説

本節のサンプルは 3-1節 のサンプルを改造したものであるため、3-1節 で説明した処理については説明せず、複数の言語に対応する方法について説明します。

複数言語への対応方法

本節のサンプルでは、UIを日本語と英語に対応させます。 本書でもUIを日本語にして説明していることから、「Blender自体が公式で日本語に対応しているため、アドオン側で特に何もしなくても自動的に翻訳してくれるのでは?」と思うかもしれません。 しかし、Blenderが日本語に対応しているのはあくまで公式の機能のみであり、個人で作成したアドオンなどは、日本語を選んだとしても日本語には変換されずに英語のまま表示されます。 このため、アドオンのUIを日本語に対応するためには、アドオン側で対応が必要になります。 具体的には、次に示す手順でアドオンを複数の言語に対応させます。

  1. 対応する言語に対する翻訳辞書を作成する
  2. 翻訳辞書を登録する
  3. 翻訳箇所の文字列を自動翻訳関数に置き換える

1. 対応する言語に対する翻訳辞書を作成する

前述しましたが、Blenderが日本語に対応しているのは公式の機能のみです。 アドオン開発者が何も対応することなく、Blenderが自動的にアドオンのUIを日本語に翻訳してくれる部分は、ほとんどないと思った方がよいでしょう。 このため、Blenderが翻訳できないところは、アドオン開発者が翻訳後(日本語)のテキストをBlenderに教えてあげる必要があります。 この時必要になってくるのが、翻訳後のテキストと翻訳前(英語)のテキストを結びつけた翻訳辞書と呼ばれるものです。 人間がわからない単語を辞書で調べて翻訳後の単語を知るのと同じように、Blenderはわからない単語(テキスト)を翻訳辞書で調べて翻訳後の単語(テキスト)を知ることができます。 ここで単語(テキスト)と書いたのは、単語だけでなくテキスト(文字列)も、翻訳辞書に登録することができるからです。

登録する翻訳辞書は辞書型の変数で、次に示す形式で指定する必要があります。 基本的には、ロケールごとにキーと翻訳語のテキストのペアを指定して、辞書を作っていきます。 このため、翻訳辞書は2段の辞書型から構成されることになります。

{
    locale: {
        (context, key) : translated_str,
        (context, key) : translated_str,
        ...
    },
    locale: {
        (context, key) : translated_str,
        ...
    }
    ...
}

上記の辞書型に指定する各パラメータの意味を次に示します。

パラメータ 意味
locale 翻訳対象のロケールを指定します。日本語なら "jp_JP" 、英語なら "en_US" を指定します。ロケールの調べ方は後述します。
context コンテキストを指定します。基本的には "*" を指定します。
key 自動翻訳関数(後述)に指定するキー文字列を指定します。同じ key に対して指定した、いずれかの locale に属する translated_str の文字列を指定するとよいと思います。もし作成したアドオンが英語をサポートするのであれば、文字化けしない英語を指定するのがおすすめです。
translated_str 翻訳後の文字列を指定します。現在のBlenderのロケールが locale と同じ、かつ自動翻訳関数にkey が指定されたとき、本パラメータに指定した文字列が表示されます。現在のBlenderのロケールがいずれの locale にも存在しない場合は、key に指定した文字列が表示されます。
パラメータ context には "*"` 以外の値も設定できるようですが、指定できる具体的な値はよくわかっていません。 本パラメータを指定する意味がわからない状態で、APIを使用するのはあまりよいとは言えませんが、とりあえず"*"` を指定しておけば正しく翻訳処理が行われるようです。

本節のサンプルでは、翻訳辞書としてグローバル変数 translation_dict を定義しています。 辞書に登録したテキストとその翻訳内容は次の通りです。 なお、key にロケールが英語のときの翻訳語の文字列を指定することで、日本語や英語以外のロケールが指定されたときに英語の文字列が表示されるようにしました。

日本語 英語 その他(keyに指定する文字列)
マウスの右クリックで面を削除 Delete Face By Right Click Delete Face By Right Click
サンプル3-7: 選択範囲外です。 Sample3-7: Out of range Sample3-7: Out of range
サンプル3-7: 面以外を選択しました。 Sample3-7: No face is selected Sample3-7: No face is selected
サンプル3-7: 面を削除しました。 Sample3-7: Deleted Face Sample3-7: Deleted Face
サンプル3-7: 削除処理を開始しました。 Sample3-7: Start deleting faces Sample3-7: Start deleting faces
サンプル3-7: %d個の面を削除しました。 Sample3-7: %d face(s) are deleted Sample3-7: %d face(s) are deleted
開始 Start Start
終了 End End
サンプル3-7: アドオン「サンプル3-7」が有効化されました。 Sample3-7: Enabled add-on 'Sample3-7' Sample3-7: Enabled add-on 'Sample3-7'
サンプル3-7: アドオン「サンプル3-7」が無効化されました。 Sample3-7: Disabled add-on 'Sample3-7' Sample3-7: Disabled add-on 'Sample3-7'

Blenderに設定されたロケールを調べる方法

ロケールは、IT用語で言語や国・地域ごとに異なる表記方法の集合のことを指します。 つまり、ロケールを変えると表示方法が指定したロケールの表記方法に従って表示されるようになります。 例えば、ロケールを日本語にした場合は、UIなどで表示される文字列が日本語になりますし、ロケールを英語にすれば英語で表示されるようになります。 Blenderもこのロケールに従って、UIやメッセージなどの表示内容を決定します。

Blenderに設定されている現在のロケールを調べるためには、[Pythonコンソール] エリアから次のコードを実行します。 コードを実行すると、現在のロケールが文字列で表示されます。 ここで表示された文字列を翻訳辞書の locale に指定します。 アドオンでサポートする全ての言語について、この方法を利用して一通り確認しておくとよいでしょう。

>>> bpy.app.translations.locale
'en_US'

2. 翻訳辞書を登録する

作成した翻訳辞書をBlenderに登録します。 サンプルでは、register 関数において bpy.app.translations.register 関数を呼び出すことで翻訳辞書を登録します。

# 翻訳辞書の登録
bpy.app.translations.register(__name__, translation_dict)

bpy.app.translations.register 関数の第1引数には、翻訳辞書の登録先モジュールを指定します。 bpy.utils.register_module の引数に __name__ を指定して自身のモジュールを登録したときと同じように、bpy.app.translations.register 関数の引数に __name__ を指定することで、自身のモジュールに対して翻訳辞書を登録することができます。 bpy.app.translations.register 関数の第2引数には、翻訳辞書を保存した変数を指定します。

なお、登録した翻訳辞書は、不要になった時点で bpy.app.translations.unregister 関数を用いて登録を解除する必要があります。 サンプルでは、アドオンを無効化したときに翻訳辞書を登録解除するため、unregister 関数の処理内で bpy.app.translations.unregister 関数を呼び出します。

3. 翻訳箇所の文字列を自動翻訳関数に置き換える

翻訳辞書の登録を行うと、Blenderの現在の言語設定(ロケール)に応じて、自動翻訳関数で指定したキーに対応した文字列が表示されるようになります。 このため、翻訳を行いたい箇所を自動翻訳関数 bpy.app.translations.pgettext で置き換えます。

本節のサンプルでは複数の箇所に自動翻訳関数を追加していますが、ここではアドオン有効化時にコンソールウィンドウへ文字列を表示する部分について説明します。

print(
    bpy.app.translations.pgettext(
        "Sample3-7: Enabled add-on 'Sample3-7'"
    )
)

自動翻訳関数 bpy.app.translations.pgettext の引数には、表示したい翻訳後の文字列 translated_str に対応する key に指定した文字列を指定します。 上記の例では、引数に "Sample3-7: Enabled add-on 'Sample3-7'" を指定することで、ロケールが英語("en_US")の場合は "Sample3-7: Enabled add-on 'Sample3-7'" が、ロケールが日本語("ja_JP")の場合は "サンプル3-7: アドオン「サンプル3-7」が有効化されました。"bpy.app.translations.pgettext 関数の戻り値として返ります。 関数の戻り値を print 関数で表示することで、ロケールに応じてコンソールウィンドウに出力される文字列が変わるため、ユーザがBlenderの言語設定を変えた時に、あたかも自動的に翻訳されているようにみえます。

なお、bpy.app.translations.pgettext を用いることで文字列の翻訳が完了しますが、次に示す処理のように文字列フォーマットによる文字列を翻訳する場合は、代わりに bpy.app.translations.pgettext_iface を用いる必要があることに注意が必要です。

print(
    bpy.app.translations.pgettext_iface(
        "Sample3-7: %d face(s) are deleted"
    )
    % (props.deleted_count)
)

まとめ

本節では、Blenderの言語設定を変更した時に、アドオンが表示するテキストを設定した言語に自動的に変更する方法を説明しました。 アドオンのUIを多言語化する上で必要な処理はそれほど多くないため、翻訳用の辞書を作成するところが一番大変かもしれません。 特に翻訳量が多い場合は、翻訳に時間を使ってしまってアドオンの開発どころではなくなってしまうかもしれません。 このため、正確さに少し難がありますが、Python向けに用意されている自動翻訳APIを利用して自動翻訳に頼ってしまう方法もあります。 翻訳数をもとに、自動翻訳を利用することを検討してみてください。

海外に向けてアドオンを提供することを考えている方は、ぜひこの機会にアドオンのUIの多言語化に挑戦してみてください。 世界共通語といってもよい英語をサポートするだけで、アドオンを使ってくれるユーザは非常に多くなりますし、海外の方からのフィードバックを得られる機会も多くなります。 このフィードバックがきっかけで、海外のBlenderユーザと知り合いになれるかもしれません。 ただし海外に向けてアドオンを提供する場合は、翻訳数が多すぎるなどよほどのことがない限り、自動翻訳を利用しない方がよいです。 自動翻訳された日本語をみて違和感を感じるのと同じように、自動翻訳によって日本語を別の言語に翻訳するとどうしても違和感のある訳になってしまうため、ユーザを混乱させてしまいます。

ポイント