mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-10 09:48:32 +00:00
Merge branch 'localization' into v0.7.0
This commit is contained in:
@@ -696,6 +696,11 @@ endif
|
||||
# Required for Desktop icon and file association
|
||||
cp -r blenderbim/libs/desktop dist/blenderbim/libs/
|
||||
|
||||
# generate translations module for BBIM build
|
||||
git clone https://github.com/IfcOpenShell/blenderbim-translations.git dist/working
|
||||
python blenderbim/scripts/bbim_translations.py -i "dist/working" -o "dist/blenderbim"
|
||||
rm -rf dist/working
|
||||
|
||||
# Remove dependencies also bundled with Blender
|
||||
rm -rf dist/blenderbim/libs/site/packages/numpy
|
||||
rm -rf dist/blenderbim/libs/site/packages/numpy.libs
|
||||
|
||||
@@ -24,6 +24,10 @@ import blenderbim
|
||||
import importlib
|
||||
from . import handler, ui, prop, operator, helper
|
||||
|
||||
try:
|
||||
from blenderbim.translations import translations_dict
|
||||
except ImportError:
|
||||
translations_dict = {}
|
||||
cwd = os.path.dirname(os.path.realpath(__file__))
|
||||
|
||||
modules = {
|
||||
@@ -250,6 +254,8 @@ def register():
|
||||
except:
|
||||
pass
|
||||
|
||||
bpy.app.translations.register("blenderbim", translations_dict)
|
||||
|
||||
|
||||
def unregister():
|
||||
global icons
|
||||
@@ -292,3 +298,5 @@ def unregister():
|
||||
bpy.utils.unregister_class(override_panel)
|
||||
bpy.utils.register_class(original_panel)
|
||||
del overridden_scene_panels[panel]
|
||||
|
||||
bpy.app.translations.unregister("blenderbim")
|
||||
|
||||
@@ -32,6 +32,10 @@ from bpy.props import (
|
||||
CollectionProperty,
|
||||
)
|
||||
|
||||
import gettext
|
||||
|
||||
_ = gettext.gettext
|
||||
|
||||
|
||||
def get_style_types(self, context):
|
||||
if not StylesData.is_loaded:
|
||||
@@ -88,9 +92,9 @@ def update_shader_graph(self, context):
|
||||
|
||||
|
||||
UV_MODES = [
|
||||
("UV", "UV", "Actual UV data presented on the geometry"),
|
||||
("Generated", "Generated", "Automatically-generated UV from the vertex positions of the mesh"),
|
||||
("Camera", "Camera", "UV from position coordinate in camera space"),
|
||||
("UV", "UV", _("Actual UV data presented on the geometry")),
|
||||
("Generated", "Generated", _("Automatically-generated UV from the vertex positions of the mesh")),
|
||||
("Camera", "Camera", _("UV from position coordinate in camera space")),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,374 @@
|
||||
try:
|
||||
import bpy
|
||||
import bl_i18n_utils
|
||||
import addon_utils
|
||||
|
||||
BPY_IS_LOADED = True
|
||||
except ModuleNotFoundError:
|
||||
BPY_IS_LOADED = False
|
||||
|
||||
import shutil
|
||||
import tempfile
|
||||
import importlib
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
bl_info = {
|
||||
"name": "BlenderBIM Translations",
|
||||
"description": "",
|
||||
"author": "IfcOpenShell Contributors",
|
||||
"blender": (2, 80, 0),
|
||||
"version": (0, 0, 999999),
|
||||
"location": "Properties -> Render -> BBIM Update Translation",
|
||||
"tracker_url": "https://github.com/IfcOpenShell/IfcOpenShell/issues",
|
||||
"category": "System",
|
||||
}
|
||||
|
||||
# NOTE: list of available languages in Blender can be retrieved
|
||||
# by the command below:
|
||||
# bpy.context.preferences.view.language = "test"
|
||||
ADDON_NAME = "blenderbim"
|
||||
TRANSLATION_UI_IS_LOADED = False
|
||||
|
||||
|
||||
def is_addon_loaded(addon_name) -> bool:
|
||||
loaded_default, loaded_state = addon_utils.check(addon_name)
|
||||
return loaded_state
|
||||
|
||||
|
||||
def get_branches_directory():
|
||||
return Path(bpy.context.preferences.filepaths.i18n_branches_directory)
|
||||
|
||||
|
||||
def get_addon_directory() -> Path:
|
||||
addon_module = importlib.import_module(ADDON_NAME)
|
||||
return Path(addon_module.__file__).parent
|
||||
|
||||
|
||||
def rearrange_files_for_po_import(po_dir_path: Path, temp_directory: tempfile.TemporaryDirectory):
|
||||
"""I18n directory also has a bit different format then `ui_translate.export/import`,
|
||||
every .po file has a parent folder with the same name.
|
||||
So we rearrange the data that way"""
|
||||
# NOTE: TemporaryDirectory is cleared automatically as variable goes out of scope
|
||||
# so we expect it as an argument so it won't get cleared right away
|
||||
|
||||
temp_po_dir_path = Path(temp_directory.name)
|
||||
for file in po_dir_path.glob("**/*"):
|
||||
if file.suffix != ".po":
|
||||
continue
|
||||
shutil.copy(file, temp_po_dir_path / file.name)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Message:
|
||||
msgid: str
|
||||
msgctxt: str | None
|
||||
sources: Optional[List[str]] = field(default_factory=list)
|
||||
# mapping languages to translated strings
|
||||
translations: Optional[Dict[str, str]] = field(default_factory=dict)
|
||||
|
||||
|
||||
def blenderbim_strings_parse(addon_directory=None, po_directory=None):
|
||||
# NOTE: we decided to use our own parser due bug in Blender parser
|
||||
# as it tends to pick up strings from other addons and other Blender parts
|
||||
# and this bug probably would be to low of a priority for Blender to fix
|
||||
# ref: https://projects.blender.org/blender/blender/issues/116579
|
||||
|
||||
if addon_directory is None:
|
||||
addon_directory = get_addon_directory()
|
||||
directory = addon_directory / "bim"
|
||||
|
||||
if po_directory is None:
|
||||
po_directory = get_branches_directory()
|
||||
|
||||
patterns = [
|
||||
r'bl_label\s*=\s*"(.*)"', # operator labels
|
||||
r'bl_description\s*=\s*"(.*)"', # operator descriptions
|
||||
r'text\s*=\s*"(.*?)"', # UI labels
|
||||
r'Property\(.*?name\s*=\s*"(.*?)"', # property names
|
||||
r'report\(\{.*?\}\s*,\s*"(.*?)"', # operator reports
|
||||
r'info\(\{.*?\}\s*,\s*"(.*?)"', # operator info
|
||||
r'\b_\("(.*?)"\)', # gettext called with `_`
|
||||
]
|
||||
regexes = [re.compile(pattern) for pattern in patterns]
|
||||
matched_dict: Dict[str, Message] = dict()
|
||||
|
||||
# NOTE: currently there is no special handling same message with different contexts
|
||||
for root, dirs, files in os.walk(directory):
|
||||
root = Path(root)
|
||||
for file in files:
|
||||
if not file.endswith(".py"):
|
||||
continue
|
||||
filepath = root / file
|
||||
source_rel_path = filepath.relative_to(addon_directory).as_posix()
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
for i, regex in enumerate(regexes):
|
||||
is_operator = i < 2
|
||||
for regex_match in regex.finditer(content):
|
||||
string = regex_match.group(1)
|
||||
if string == "":
|
||||
continue
|
||||
elif "{" in string:
|
||||
# Includes a formatting string. To fix.
|
||||
# print(match.group(1))
|
||||
continue
|
||||
elif is_operator:
|
||||
ctx = "Operator"
|
||||
else:
|
||||
ctx = None
|
||||
message = matched_dict.get(string)
|
||||
if message is None:
|
||||
message = Message(string, ctx)
|
||||
matched_dict[string] = message
|
||||
elif ctx != message.msgctxt and False:
|
||||
print(
|
||||
f'WARNING. Message "{string}" was already registered with different context {message.context}. '
|
||||
f"Current context: {ctx}. File: {source_rel_path}"
|
||||
)
|
||||
message.sources.append(source_rel_path)
|
||||
|
||||
pot_filepath = po_directory / "blenderbim.pot"
|
||||
with open(pot_filepath, "w", encoding="utf-8") as fo:
|
||||
for msg in matched_dict.values():
|
||||
# make sure sources are unique but keep the order
|
||||
sources = list(dict.fromkeys(msg.sources))
|
||||
for source in sources:
|
||||
fo.write(f"#: {source}\n")
|
||||
if msg.msgctxt != None:
|
||||
fo.write(f'msgctxt "{msg.msgctxt}"\n')
|
||||
fo.write(f'msgid "{msg.msgid}"\n')
|
||||
fo.write('msgstr ""\n')
|
||||
fo.write("\n")
|
||||
|
||||
|
||||
def update_translations_from_po(po_directory: Path, translations_module: Path):
|
||||
translation_data: Dict[str, Message] = dict()
|
||||
|
||||
def process_po_entry(language, current_chunk: list[str]):
|
||||
sources = []
|
||||
msgid = None
|
||||
msgstr = None
|
||||
msgctxt = None
|
||||
parse_message_attr = lambda line, attr_name: line.removeprefix(f'{attr_name} "').removesuffix('"')
|
||||
|
||||
for line in current_chunk:
|
||||
line = line.strip()
|
||||
if line.startswith('msgid "'):
|
||||
msgid = parse_message_attr(line, "msgid")
|
||||
elif line.startswith('msgstr "'):
|
||||
msgstr = parse_message_attr(line, "msgstr")
|
||||
elif line.startswith('msgctxt "'):
|
||||
msgctxt = parse_message_attr(line, "msgctxt")
|
||||
elif line.startswith("#:"):
|
||||
sources.append(line.removeprefix("# ").strip())
|
||||
|
||||
msg = translation_data.get(msgid)
|
||||
if msg is None:
|
||||
msg = Message(msgid, msgctxt, sources, {language: msgstr})
|
||||
translation_data[msgid] = msg
|
||||
else:
|
||||
msg.sources.extend(sources)
|
||||
msg.translations[language] = msgstr
|
||||
|
||||
# load data from .po files
|
||||
langs = set()
|
||||
for po_file_path in po_directory.glob("**/*.po"):
|
||||
lang = po_file_path.stem
|
||||
langs.add(lang)
|
||||
with open(po_file_path, "r") as po_file:
|
||||
current_chunk = []
|
||||
for line in po_file:
|
||||
current_chunk.append(line)
|
||||
if line.startswith("msgstr"):
|
||||
process_po_entry(lang, current_chunk)
|
||||
current_chunk = []
|
||||
|
||||
# generate translations.py file
|
||||
tab = " "
|
||||
default_context = "*"
|
||||
ret = ["translations_dict = {"]
|
||||
|
||||
for lang in langs:
|
||||
ret.append(f'{tab}"{lang}": {{')
|
||||
for msgid, msg in translation_data.items():
|
||||
if (msgstr := msg.translations[lang]) in (None, ""):
|
||||
continue
|
||||
msgctxt = msg.msgctxt
|
||||
if not msgctxt:
|
||||
msgctxt = default_context
|
||||
ret.append(f"{tab*2}({msgctxt!r}, {msgid!r}): {msgstr!r},")
|
||||
ret.append(f"{tab}}},")
|
||||
|
||||
ret.append("}")
|
||||
|
||||
with open(translations_module / "translations.py", "w") as fo:
|
||||
fo.write("\n".join(ret))
|
||||
|
||||
|
||||
if BPY_IS_LOADED:
|
||||
|
||||
class SetupTranslationUI(bpy.types.Operator):
|
||||
bl_idname = "bim.setup_translation_ui"
|
||||
bl_label = "Setup Translation UI"
|
||||
bl_options = set()
|
||||
|
||||
def execute(self, context):
|
||||
if not is_addon_loaded("ui_translate"):
|
||||
addon_utils.enable("ui_translate", default_set=True)
|
||||
self.report({"INFO"}, '"Manage UI translations" addon was not enabled, it\'s enabled now.')
|
||||
|
||||
addon_prefs = context.preferences.addons["ui_translate"].preferences
|
||||
|
||||
def is_valid_path(path: Path):
|
||||
return path.is_dir() and path.is_absolute()
|
||||
|
||||
# check branches directory
|
||||
branches_dir = get_branches_directory()
|
||||
if not is_valid_path(branches_dir):
|
||||
raise Exception(
|
||||
f"I18n Branches directory is not set up or doesn't exist ({branches_dir}).\n"
|
||||
"Setup I18n branches directory below (or in Preferences > File Paths > Development > I18n Branches) "
|
||||
"to a folder containing (or that will contain) .po files."
|
||||
)
|
||||
|
||||
# check translations directory
|
||||
i18n_dir = Path(addon_prefs.I18N_DIR)
|
||||
# we won't really use the translations directory, we just need addon to stop complaining about it
|
||||
if not is_valid_path(i18n_dir):
|
||||
addon_prefs.I18N_DIR = str(branches_dir)
|
||||
self.report(
|
||||
{"INFO"},
|
||||
f'Translations directory ({i18n_dir}) doesn\'t exist. It was reset to I18n branches directory: "{branches_dir}"',
|
||||
)
|
||||
|
||||
# check source directory
|
||||
source_dir = Path(addon_prefs.SOURCE_DIR)
|
||||
default_source_path = Path(bpy.app.binary_path).parent / ".".join([str(i) for i in bpy.app.version[:2]])
|
||||
|
||||
if not is_valid_path(source_dir):
|
||||
addon_prefs.SOURCE_DIR = str(default_source_path)
|
||||
self.report(
|
||||
{"INFO"},
|
||||
f'Source directory ({source_dir}) doesn\'t exist. It was reset to default directory: "{default_source_path}"',
|
||||
)
|
||||
source_dir = default_source_path
|
||||
|
||||
source_locale_path = source_dir / "locale/po"
|
||||
# Blender UI translations also expect "scripts/presets/keyconfig" to be present in SOURCE_DIR
|
||||
# but default directory has it by default
|
||||
if not source_locale_path.is_dir():
|
||||
source_locale_path.mkdir(parents=True, exist_ok=True)
|
||||
self.report(
|
||||
{"INFO"},
|
||||
f"Couldn't find locale path in the source directory, creating dummy directory: {source_locale_path}.",
|
||||
)
|
||||
|
||||
from ui_translate.settings import settings as ui_translate_settings
|
||||
from ui_translate.update_ui import UI_OT_i18n_updatetranslation_init_settings
|
||||
|
||||
i18n_settings = context.window_manager.i18n_update_settings
|
||||
if not i18n_settings.is_init:
|
||||
# if it's not loaded yet, we'll try to reload it one more time
|
||||
# since we default values we set up during the current operator might helped
|
||||
UI_OT_i18n_updatetranslation_init_settings.execute_static(context, ui_translate_settings)
|
||||
if not i18n_settings.is_init:
|
||||
raise Exception(
|
||||
"UI Translation settings are not initalized. Make sure the following directories exist:\n"
|
||||
f" - {ui_translate_settings.WORK_DIR}\n"
|
||||
f" - {ui_translate_settings.BLENDER_I18N_PO_DIR}\n"
|
||||
)
|
||||
|
||||
global TRANSLATION_UI_IS_LOADED
|
||||
TRANSLATION_UI_IS_LOADED = True
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
class ParseBlenderBIMStrings(bpy.types.Operator):
|
||||
bl_idname = "bim.parse_blenderbim_strings"
|
||||
bl_label = "Parse BlenderBIM strings To .pot"
|
||||
bl_description = "Parse strings from BlenderBIM and save to .pot"
|
||||
bl_options = set()
|
||||
|
||||
def execute(self, context):
|
||||
blenderbim_strings_parse()
|
||||
self.report({"INFO"}, "String were parsed and saved to .pot file.")
|
||||
return {"FINISHED"}
|
||||
|
||||
class UpdateTranslationsFromPo(bpy.types.Operator):
|
||||
bl_idname = "bim.update_translations_from_po"
|
||||
bl_label = "Update Translations From .po"
|
||||
bl_description = (
|
||||
"Load translation strings from po files at I18n Branches back to translations.py\n"
|
||||
"Also updates current addon translations in UI"
|
||||
)
|
||||
bl_options = set()
|
||||
|
||||
def execute(self, context):
|
||||
branches_dir = get_branches_directory()
|
||||
update_translations_from_po(branches_dir, get_addon_directory())
|
||||
|
||||
if is_addon_loaded(ADDON_NAME):
|
||||
bpy.app.translations.unregister(ADDON_NAME)
|
||||
addon_module = importlib.import_module(ADDON_NAME)
|
||||
translations_module = getattr(addon_module, "translations")
|
||||
importlib.reload(translations_module)
|
||||
bpy.app.translations.register(ADDON_NAME, translations_module.translations_dict)
|
||||
|
||||
self.report({"INFO"}, f"Addon's translation updated from .po in {branches_dir}")
|
||||
return {"FINISHED"}
|
||||
|
||||
class BBIM_PT_translations(bpy.types.Panel):
|
||||
bl_label = "BlenderBIM Translations"
|
||||
bl_idname = "BBIM_PT_translations"
|
||||
bl_space_type = "PROPERTIES"
|
||||
bl_region_type = "WINDOW"
|
||||
bl_context = "render"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.prop(context.preferences.view, "language")
|
||||
layout.prop(context.preferences.filepaths, "i18n_branches_directory")
|
||||
|
||||
if not TRANSLATION_UI_IS_LOADED:
|
||||
layout.operator("bim.setup_translation_ui")
|
||||
return
|
||||
|
||||
layout.label(text="Developer UI:")
|
||||
layout.operator("bim.parse_blenderbim_strings", icon="FILE_REFRESH")
|
||||
|
||||
layout.separator()
|
||||
layout.label(text="Translator UI:")
|
||||
layout.operator("bim.update_translations_from_po", icon="IMPORT")
|
||||
|
||||
classes = (
|
||||
ParseBlenderBIMStrings,
|
||||
UpdateTranslationsFromPo,
|
||||
SetupTranslationUI,
|
||||
BBIM_PT_translations,
|
||||
)
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
def unregister():
|
||||
for cls in classes:
|
||||
bpy.utils.unregister_class(cls)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Example:
|
||||
# py src/blenderbim/scripts/bbim_translations.py -i "C:/blenderbim-translations" -o "C:/Blender/4.0/scripts/addons/blenderbim/translations.py"
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="Converts .po files to translations.py")
|
||||
parser.add_argument("-i", "--input", type=str, required=True, help="Directory with .po files")
|
||||
parser.add_argument(
|
||||
"-o", "--output", type=str, required=True, help="translations.py module location (file may not exist yet)"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
update_translations_from_po(po_directory=Path(args.input), translations_module=Path(args.output))
|
||||
Reference in New Issue
Block a user