五种翻译机制对比
- 全局翻译 (Global Translation)
-
原理 :替换 整个系统的翻译文件
-
特点 :一次性覆盖,需要重启生效
-
缺点 :不够灵活,可能影响其他翻译
2.PO/MO翻译 (标准gettext翻译)mo字典翻译任何blender插件的插件
-
原理 :使用GNU gettext的PO/MO 文件格式
-
特点 :Blender官方标准,支持多语言
-
缺点 :需要编译MO文件,更新复杂
- 字典翻译 (Dictionary Translation)
-
原理 :建立原文-译文的映射字典
-
特点 :简单直接,易于维护
-
缺点 :覆盖范围有限,需要手动维护
- 拦截翻译 (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()
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}")
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()