五种翻译--mo字典翻译任何blender插件的插件

五种翻译机制对比

  1. 全局翻译 (Global Translation)
  • 原理 :替换 整个系统的翻译文件

  • 特点 :一次性覆盖,需要重启生效

  • 缺点 :不够灵活,可能影响其他翻译

2.PO/MO翻译 (标准gettext翻译)mo字典翻译任何blender插件的插件

  • 原理 :使用GNU gettext的PO/MO 文件格式

  • 特点 :Blender官方标准,支持多语言

  • 缺点 :需要编译MO文件,更新复杂

  1. 字典翻译 (Dictionary Translation)
  • 原理 :建立原文-译文的映射字典

  • 特点 :简单直接,易于维护

  • 缺点 :覆盖范围有限,需要手动维护

  1. 拦截翻译 (Real-time Hook Translation) ✅
  • 原理 : 运行时拦截系统函数调用 ,实时翻译文本

  • 特点 :

  • 无需文件操作 :直接在内存中翻译

  • 实时生效 :不需要重启(部分需要)

  • 智能分段 :支持复杂文本的模式匹配

  • 缓存优化 :避免重复翻译

  • 技术实现 :Hook系统函数 + 正则匹配 + 缓存机制

5,.语言切换器 :只是切换开关 :控制Blender内置翻译系统的启用/禁用

  • 不替换翻译文件 :

-没有 .mo 、 .po 文件操作

  • 不拦截文本 :没有运行时文本替换机制

-没有内置字典

  • 只是切换开关 :控制Blender内置翻译系统的启用/禁用

翻译任何插件

init.py

python 复制代码
bl_info = {
    "name": "GlobalTrans",
    "author": "zsz",
    "version": (1, 0, 0),
    "blender": (4, 0, 0),
    "location": "View3D > Sidebar > GlobalTrans",
    "description": "全局翻译测试插件",
    "category": "Interface",
}

import bpy
import os

# 翻译域名称
translation_domain = "MOPO_trans_addon"

class GLOBALTRANS_PT_main_panel(bpy.types.Panel):
    bl_label = "GlobalTrans"
    bl_idname = "GLOBALTRANS_PT_main_panel"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    bl_category = "GlobalTrans"
    
    def draw(self, context):
        layout = self.layout
        
        # 显示当前语言设置
        layout.label(text="当前blendr语言模式:")
        layout.label(text=bpy.app.translations.locale)
        
        # 显示当前语言下的翻译示例
        layout.separator()
        layout.label(text="当前语言翻译内容:")
        
        # Shader的当前翻译
        box = layout.box()
        box.label(text=bpy.app.translations.pgettext("Shader AOV"))
        
        # Action的当前翻译
        box = layout.box()
        box.label(text=bpy.app.translations.pgettext("Action"))


def register():
    # 注册翻译域
    try:
        # 获取插件目录路径
        addon_path = os.path.dirname(os.path.abspath(__file__))
        locales_path = os.path.join(addon_path, "locales")
        
        # 使用Blender 4.0+的翻译域注册方式
        bpy.app.translations.register(translation_domain, locales_path)
    except Exception as e:
        print(f"注册翻译域失败: {e}")
    
    # 注册面板类
    try:
        bpy.utils.register_class(GLOBALTRANS_PT_main_panel)
    except ValueError:
        pass  # 类已经被注册,忽略错误


def unregister():
    try:
        bpy.utils.unregister_class(GLOBALTRANS_PT_main_panel)
    except ValueError:
        pass  # 类已经被注销,忽略错误
    
    # 注销翻译域
    try:
        bpy.app.translations.unregister(translation_domain)
    except Exception as e:
        print(f"注销翻译域失败: {e}")


if __name__ == "__main__":
    register()

DoPO.py

python 复制代码
import os
import re
import datetime

def extract_strings_from_python_file(file_path):
    """
    从Python文件中提取需要翻译的字符串
    
    Args:
        file_path: Python文件路径
        
    Returns:
        list: 提取的字符串列表
    """
    if not os.path.exists(file_path):
        return []
    
    strings = set()
    
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            content = f.read()
        
        # 提取bl_label = "..."
        bl_label_pattern = r'bl_label\s*=\s*"([^"]+)"'
        strings.update(re.findall(bl_label_pattern, content))
        
        # 提取bl_description = "..."
        bl_description_pattern = r'bl_description\s*=\s*"([^"]+)"'
        strings.update(re.findall(bl_description_pattern, content))
        
        # 提取pgettext_iface("...")
        pgettext_pattern = r'bpy\.app\.translations\.pgettext_iface\("([^"]+)"\)'
        strings.update(re.findall(pgettext_pattern, content))
        
        # 提取pgettext("...")
        pgettext_pattern = r'bpy\.app\.translations\.pgettext\("([^"]+)"\)'
        strings.update(re.findall(pgettext_pattern, content))
        
        # 提取普通字符串翻译(_() 或 __())
        gettext_pattern = r'_\("([^"]+)"\)|__\("([^"]+)"\)'
        matches = re.findall(gettext_pattern, content)
        for match in matches:
            if match[0]:
                strings.add(match[0])
            if match[1]:
                strings.add(match[1])
        
        # 提取面板中的标签文本
        label_pattern = r'layout\.label\s*\(text\s*=\s*"([^"]+)"\)'
        strings.update(re.findall(label_pattern, content))
        
        # 提取操作按钮文本
        operator_pattern = r'layout\.operator\s*\([^\)]+\s*text\s*=\s*"([^"]+)"\)'
        strings.update(re.findall(operator_pattern, content))
        
    except Exception as e:
        print(f"从文件 {file_path} 提取字符串失败: {e}")
        return []
    
    # 过滤掉空字符串并排序
    return sorted([s for s in strings if s])

def create_po_file(po_file_path, extracted_strings, package_name="realpomotrans", version="1.0", language="zh_CN"):
    """
    创建PO文件
    
    Args:
        po_file_path: PO文件路径
        extracted_strings: 提取的字符串列表
        package_name: 包名
        version: 版本
        language: 目标语言
        
    Returns:
        bool: 创建是否成功
        str: 错误信息(如果失败)
    """
    try:
        # 创建PO文件内容
        po_content = f'''# Translation file for {package_name}
# Copyright (C) {datetime.datetime.now().year} {package_name} developers
# This file is distributed under the same license as the {package_name} package.
#
msgid ""
msgstr ""
"Project-Id-Version: {package_name} {version}\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M%z')}\n"
"PO-Revision-Date: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M%z')}\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: {language}\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"
\n'''
        
        # 添加每个提取的字符串
        for string in extracted_strings:
            po_content += f'msgid "{string}"\nmsgstr ""\n\n'
        
        # 确保目录存在
        po_dir = os.path.dirname(po_file_path)
        if po_dir and not os.path.exists(po_dir):
            os.makedirs(po_dir)
        
        # 写入PO文件
        with open(po_file_path, 'w', encoding='utf-8') as po_file:
            po_file.write(po_content)
        
        return True, f"PO文件已创建: {po_file_path}"
        
    except Exception as e:
        return False, f"创建PO文件失败: {str(e)}"

def process_directory(directory, po_file_path, package_name="realpomotrans", version="1.0", language="zh_CN"):
    """
    处理目录中的所有Python文件,提取翻译字符串并创建PO文件
    
    Args:
        directory: 要处理的目录
        po_file_path: PO文件路径
        package_name: 包名
        version: 版本
        language: 目标语言
        
    Returns:
        bool: 处理是否成功
        str: 错误信息(如果失败)
    """
    if not os.path.exists(directory):
        return False, f"目录不存在: {directory}"
    
    all_strings = set()
    
    # 遍历目录中的所有Python文件
    for root, dirs, files in os.walk(directory):
        for file in files:
            if file.endswith('.py'):
                file_path = os.path.join(root, file)
                strings = extract_strings_from_python_file(file_path)
                all_strings.update(strings)
    
    if not all_strings:
        return False, "没有提取到需要翻译的字符串"
    
    # 创建PO文件
    return create_po_file(po_file_path, sorted(all_strings), package_name, version, language)

def process_python_files_and_generate_translations(python_files, translation_domain, language="zh_HANS"):
    """
    从指定的Python文件列表中提取翻译字符串,自动创建目录结构并生成mo文件
    
    Args:
        python_files: Python文件路径列表
        translation_domain: 翻译域名称
        language: 目标语言代码
        
    Returns:
        bool: 处理是否成功
        str: 错误信息(如果失败)
    """
    try:
        # 检查文件列表是否为空
        if not python_files:
            return False, "没有提供要处理的Python文件"
        
        # 1. 从所有指定的Python文件中提取翻译字符串
        all_extracted_strings = set()
        for file_path in python_files:
            if not os.path.exists(file_path):
                print(f"警告:文件不存在,跳过处理: {file_path}")
                continue
            
            strings = extract_strings_from_python_file(file_path)
            all_extracted_strings.update(strings)
        
        if not all_extracted_strings:
            return False, "没有从指定文件中提取到需要翻译的字符串"
        
        # 2. 创建locales目录结构
        # 获取第一个有效文件的目录作为插件根目录
        valid_file_path = next((f for f in python_files if os.path.exists(f)), None)
        if not valid_file_path:
            return False, "没有找到有效的Python文件"
            
        addon_dir = os.path.dirname(valid_file_path)
        locales_dir = os.path.join(addon_dir, "locales")
        lang_dir = os.path.join(locales_dir, language)
        lc_messages_dir = os.path.join(lang_dir, "LC_MESSAGES")
        
        # 创建目录结构
        os.makedirs(lc_messages_dir, exist_ok=True)
        
        # 3. 创建PO文件
        po_file_path = os.path.join(lc_messages_dir, f"{translation_domain}.po")
        success, message = create_po_file(po_file_path, sorted(all_extracted_strings), translation_domain, "1.0", language)
        if not success:
            return success, message
        
        # 4. 调用po2mo.py生成MO文件
        po2mo_path = os.path.join(addon_dir, "po2mo.py")
        if not os.path.exists(po2mo_path):
            return False, f"找不到po2mo.py文件: {po2mo_path}"
        
        # 切换到LC_MESSAGES目录执行po2mo.py
        import subprocess
        original_cwd = os.getcwd()
        os.chdir(lc_messages_dir)
        
        # 使用Blender的Python执行po2mo.py,并传递PO文件路径参数
        # 尝试自动查找Blender Python路径
        blender_python_path = None
        
        # 常见的Blender Python安装路径
        common_paths = [
            r"D:\Program Files\Blender Foundation\blender-5.0\5.0\python\bin\python.exe",
            r"C:\Program Files\Blender Foundation\blender-5.0\5.0\python\bin\python.exe",
            r"D:\Program Files\Blender Foundation\blender-4.0\4.0\python\bin\python.exe",
            r"C:\Program Files\Blender Foundation\blender-4.0\4.0\python\bin\python.exe",
            r"D:\Program Files\Blender Foundation\blender-3.6\3.6\python\bin\python.exe",
            r"C:\Program Files\Blender Foundation\blender-3.6\3.6\python\bin\python.exe",
        ]
        
        for path in common_paths:
            if os.path.exists(path):
                blender_python_path = path
                break
        
        # 如果没有找到,提示用户
        if not blender_python_path:
            return False, "找不到Blender Python路径,请手动修改脚本中的blender_python_path变量"
        
        result = subprocess.run([blender_python_path, po2mo_path, po_file_path], capture_output=True, text=True)
        
        os.chdir(original_cwd)
        
        if result.returncode != 0:
            return False, f"po2mo.py执行失败: {result.stderr}"
        
        return True, f"翻译文件生成成功!\nPO文件: {po_file_path}\nMO文件: {os.path.join(lc_messages_dir, f'{translation_domain}.mo')}"
        
    except Exception as e:
        return False, f"处理失败: {str(e)}"


def process_init_py_and_generate_translations(init_py_path, translation_domain, language="zh_HANS"):
    """
    从__init__.py提取翻译字符串,自动创建目录结构并生成mo文件(兼容旧版本)
    
    Args:
        init_py_path: __init__.py文件路径
        translation_domain: 翻译域名称
        language: 目标语言代码
        
    Returns:
        bool: 处理是否成功
        str: 错误信息(如果失败)
    """
    return process_python_files_and_generate_translations([init_py_path], translation_domain, language)

# 测试函数
if __name__ == "__main__":
    import sys
    
    # 获取当前目录
    current_dir = os.path.dirname(os.path.abspath(__file__))
    
    # 命令行参数处理
    # 如果提供了文件列表参数,则使用这些文件
    # 否则默认处理当前目录下的所有Python文件
    if len(sys.argv) > 1:
        # 第一个参数是脚本名,后面的都是要处理的文件路径
        python_files = sys.argv[1:]
        # 转换为绝对路径
        python_files = [os.path.abspath(f) if not os.path.isabs(f) else f for f in python_files]
    else:
        # 默认处理当前目录下的所有Python文件
        python_files = []
        for file in os.listdir(current_dir):
            if file.endswith('.py') and file != '__pycache__':
                python_files.append(os.path.join(current_dir, file))
    
    # 使用第一个Python文件的文件名作为翻译域(去除.py后缀)
    if python_files:
        first_file = os.path.basename(python_files[0])
        translation_domain = os.path.splitext(first_file)[0]
    else:
        translation_domain = "default_translation_domain"
    
    print(f"要处理的文件: {python_files}")
    print(f"使用的翻译域: {translation_domain}")
    
    success, message = process_python_files_and_generate_translations(python_files, translation_domain)
    print(f"处理结果: {success}, 消息: {message}")

po2mo.py

python 复制代码
# -*- coding: utf-8 -*-
# Blender 5.0/4.x/3.x 通用 PO转MO 编译脚本
# 你的命令直接运行:"D:\Program Files\Blender Foundation\blender-5.0\5.0\python\bin\python.exe" po2mo.py
# 或者指定PO文件路径:"D:\Program Files\Blender Foundation\blender-5.0\5.0\python\bin\python.exe" po2mo.py path/to/your.po
import os
import sys

# PO和MO文件名,和你的插件保持一致,不用改!
DEFAULT_PO_FILE_NAME = "MOPO_trans_addon.po"
DEFAULT_MO_FILE_NAME = "MOPO_trans_addon.mo"

def main(po_file_path=None):
    # 如果没有提供PO文件路径,则使用默认文件名
    if po_file_path is None:
        po_file_path = DEFAULT_PO_FILE_NAME
    
    # 判断PO文件是否存在
    if not os.path.exists(po_file_path):
        print(f"错误:找不到 {po_file_path} 文件!")
        return

    # 读取PO文件,提取 原文(msgid) 和 译文(msgstr)
    trans_dict = {}
    current_id = ""
    with open(po_file_path, "r", encoding="utf-8") as f:
        for line in f.readlines():
            line = line.strip()
            if line.startswith('msgid "') and not line.startswith('msgid_plural'):
                current_id = line.replace('msgid "', '').replace('"', '')
            elif line.startswith('msgstr "') and current_id:
                current_str = line.replace('msgstr "', '').replace('"', '')
                if current_str: # 只保存有翻译的内容
                    trans_dict[current_id] = current_str
                current_id = ""

    print(f"成功读取PO文件,共找到 {len(trans_dict)} 条翻译内容")
    
    # 生成MO文件路径(与PO文件在同一目录中)
    po_dir = os.path.dirname(po_file_path)
    mo_file_name = os.path.splitext(os.path.basename(po_file_path))[0] + ".mo"
    mo_file_path = os.path.join(po_dir, mo_file_name)
    
    # 生成标准MO文件(Blender原生识别,完美兼容)
    with open(mo_file_path, "wb") as mo_file:
        # 写入Blender完美兼容的MO文件头部 (核心修复点!)
        mo_file.write(b"\xde\x12\x04\x95")  # MO文件魔数,固定不变
        mo_file.write(b"\x00\x00\x00\x00")  # 版本号
        mo_file.write(len(trans_dict).to_bytes(4, "little"))  # 翻译条目数
        mo_file.write(len(trans_dict).to_bytes(4, "little"))  # 同样条目数
        mo_file.write((28 + len(trans_dict)*16).to_bytes(4, "little")) # 偏移量
        mo_file.write(b"\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00")

        # 写入翻译内容体
        offset = 28 + len(trans_dict)*16
        # 第一步:写入msgid的长度和偏移
        for msgid in trans_dict:
            mo_file.write(len(msgid.encode()).to_bytes(4, "little"))
            mo_file.write(b"\x00\x00\x00\x00")
            mo_file.write(offset.to_bytes(4, "little"))
            mo_file.write(b"\x00\x00\x00\x00")
            offset += len(msgid.encode()) + 1
        # 第二步:写入msgstr的长度和偏移
        for msgstr in trans_dict.values():
            mo_file.write(len(msgstr.encode()).to_bytes(4, "little"))
            mo_file.write(b"\x00\x00\x00\x00")
            mo_file.write(offset.to_bytes(4, "little"))
            mo_file.write(b"\x00\x00\x00\x00")
            offset += len(msgstr.encode()) + 1
        # 第三步:写入实际的翻译文本
        for msgid in trans_dict:
            mo_file.write(msgid.encode("utf-8") + b"\x00")
        for msgstr in trans_dict.values():
            mo_file.write(msgstr.encode("utf-8") + b"\x00")

    print(f"\n编译成功!MO文件已生成:{os.path.abspath(mo_file_path)}")

if __name__ == "__main__":
    # 处理命令行参数
    if len(sys.argv) > 1:
        # 如果提供了PO文件路径参数,则使用该路径
        main(sys.argv[1])
    else:
        # 否则使用默认文件名
        main()
相关推荐
Rabbit_QL8 小时前
【水印添加工具】从零设计一个工程级 Python 图片水印工具:WaterMask 架构与实现
开发语言·python
曲幽9 小时前
FastAPI多进程部署:定时任务重复执行?手把手教你用锁搞定
redis·python·fastapi·web·lock·works
森屿~~10 小时前
AI 手势识别系统:踩坑与实现全记录 (PyTorch + MediaPipe)
人工智能·pytorch·python
忧郁的橙子.10 小时前
26期_01_Pyhton文件的操作
开发语言·python
小CC吃豆子11 小时前
Python爬虫
开发语言·python
June bug12 小时前
(#字符串处理)字符串中第一个不重复的字母
python·leetcode·面试·职场和发展·跳槽
lixzest12 小时前
PyTorch基础知识简述
人工智能·pytorch·python
飞Link12 小时前
深度学习里程碑:ResNet(残差网络)从理论到实战全解析
人工智能·python·深度学习
ASS-ASH13 小时前
霸王色霸气的本质概括分析
人工智能·python·机器学习·大脑·脑电波
ValidationExpression13 小时前
学习:词嵌入(Word Embedding / Text Embedding)技术
python·学习·ai