From 925964ee2a9d68faba2731760a1afe7e6ff4fcc9 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 29 Dec 2023 15:33:28 +0500 Subject: [PATCH 01/37] BBIM script to reload/export/import translation strings #889 --- src/blenderbim/scripts/setup_translations.py | 100 +++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 src/blenderbim/scripts/setup_translations.py diff --git a/src/blenderbim/scripts/setup_translations.py b/src/blenderbim/scripts/setup_translations.py new file mode 100644 index 0000000000..4c0c31f066 --- /dev/null +++ b/src/blenderbim/scripts/setup_translations.py @@ -0,0 +1,100 @@ +import bpy +import addon_utils +from pathlib import Path +import shutil + +context = bpy.context +SUPPORT_LANGUAGES = ["ru_RU", "de_DE"] +ADDON_NAME = "localization_test" +BRANCHES_DIR = Path(context.preferences.filepaths.i18n_branches_directory) +_LOCALE_DIR = None + + +def is_addon_loaded(addon_name): + loaded_default, loaded_state = addon_utils.check(addon_name) + return loaded_state + + +def reload_translations(): + """Parse strings from Blender objects of the addon to `translations.py`""" + if is_addon_loaded(ADDON_NAME): + # ref: https://projects.blender.org/blender/blender/issues/116579 + raise Exception( + f"'{ADDON_NAME}' addon is enabled.\n" + "Need to disable it, restart Blender and start reloading translations again.\n" + "Otherwise some strings to translate might get lost due Blender bug." + ) + + bpy.ops.ui.i18n_addon_translation_update("INVOKE_DEFAULT", module_name=ADDON_NAME) + + +def get_locale_dir() -> Path: + global _LOCALE_DIR + if _LOCALE_DIR is None: + addon_dir = Path(bpy.utils.script_path_user()) / "addons" / ADDON_NAME + _LOCALE_DIR = addon_dir / "locale" + _LOCALE_DIR.mkdir(exist_ok=True) + return _LOCALE_DIR + + +def convert_translations_to_po(): + """extract current translation strings from translation.py to .po files and saved them in + both `locale` folder in addon's directory and I18n Branches directory""" + + locale_dir = get_locale_dir() + + if not BRANCHES_DIR.is_dir(): + raise Exception(f"I18n Branches directory doesn't exist: {BRANCHES_DIR.as_posix()}") + + bpy.ops.ui.i18n_addon_translation_export( + module_name=ADDON_NAME, directory=locale_dir.as_posix(), use_export_pot=False + ) + + # NOTE: we also setup I18n branches directory + # because it later will be used to edit translation from UI + for file in locale_dir.iterdir(): + if file.suffix != ".po": + continue + branches_subdir = BRANCHES_DIR / file.stem + branches_subdir.mkdir(exist_ok=True) + shutil.copy(file, branches_subdir / file.name) + + +def update_translations_from_po(): + """load translation strings from po files at I18n Branches + back to translations.py (they also get copied to `locale` directory of the addon) + """ + locale_dir = get_locale_dir() + for file in BRANCHES_DIR.glob("**/*"): + if file.suffix != ".po": + continue + shutil.copy(file, locale_dir / file.name) + + bpy.ops.ui.i18n_addon_translation_import( + module_name=ADDON_NAME, + directory=locale_dir.as_posix(), + ) + + +if __name__ == "__main__": + if not is_addon_loaded("ui_translate"): + raise Exception('"Manage UI translations" addon is not enabled') + + from ui_translate.settings import settings as ui_translate_settings + + i18n_settings = context.window_manager.i18n_update_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" + ) + + # setup selected languages + for lang in i18n_settings.langs: + lang.use = lang.uid in SUPPORT_LANGUAGES + + # uncomment the action you want to perform: + # reload_translations() + # convert_translations_to_po() + # update_translations_from_po() From 3928dd7c9c78ccb6e2bddc5bdb5ec356a172c48a Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 29 Dec 2023 15:33:38 +0500 Subject: [PATCH 02/37] BBIM - register translation strings #889 --- src/blenderbim/blenderbim/bim/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/blenderbim/blenderbim/bim/__init__.py b/src/blenderbim/blenderbim/bim/__init__.py index e6e97ac378..a60769ec16 100644 --- a/src/blenderbim/blenderbim/bim/__init__.py +++ b/src/blenderbim/blenderbim/bim/__init__.py @@ -22,6 +22,7 @@ import bpy import bpy.utils.previews import blenderbim import importlib +from blenderbim.translations import translations_dict from . import handler, ui, prop, operator, helper cwd = os.path.dirname(os.path.realpath(__file__)) @@ -249,6 +250,8 @@ def register(): except: pass + bpy.app.translations.register("blenderbim", translations_dict) + def unregister(): global icons @@ -291,3 +294,5 @@ def unregister(): bpy.utils.unregister_class(override_panel) bpy.utils.register_class(original_panel) del overridden_scene_panels[panel] + + bpy.app.translations.register("blenderbim", translations_dict) From ca4181c7140b4de51d572c17ce664d7636f9a0d9 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 29 Dec 2023 16:58:25 +0500 Subject: [PATCH 03/37] fixed typo --- src/blenderbim/blenderbim/bim/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blenderbim/blenderbim/bim/__init__.py b/src/blenderbim/blenderbim/bim/__init__.py index a60769ec16..64fbca60d3 100644 --- a/src/blenderbim/blenderbim/bim/__init__.py +++ b/src/blenderbim/blenderbim/bim/__init__.py @@ -295,4 +295,4 @@ def unregister(): bpy.utils.register_class(original_panel) del overridden_scene_panels[panel] - bpy.app.translations.register("blenderbim", translations_dict) + bpy.app.translations.unregister("blenderbim") From 82b20066866494c1ff36a4422e5fa9b77e4ac516 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 29 Dec 2023 16:54:29 +0500 Subject: [PATCH 04/37] setup_translations.py - use just 1 directory to store .po files and use temp directory to store .po files for ui_translate.export/import --- src/blenderbim/scripts/setup_translations.py | 47 +++++++++----------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/src/blenderbim/scripts/setup_translations.py b/src/blenderbim/scripts/setup_translations.py index 4c0c31f066..e5d1c9bc48 100644 --- a/src/blenderbim/scripts/setup_translations.py +++ b/src/blenderbim/scripts/setup_translations.py @@ -1,13 +1,13 @@ import bpy import addon_utils -from pathlib import Path import shutil +import tempfile +from pathlib import Path context = bpy.context SUPPORT_LANGUAGES = ["ru_RU", "de_DE"] ADDON_NAME = "localization_test" BRANCHES_DIR = Path(context.preferences.filepaths.i18n_branches_directory) -_LOCALE_DIR = None def is_addon_loaded(addon_name): @@ -28,53 +28,48 @@ def reload_translations(): bpy.ops.ui.i18n_addon_translation_update("INVOKE_DEFAULT", module_name=ADDON_NAME) -def get_locale_dir() -> Path: - global _LOCALE_DIR - if _LOCALE_DIR is None: - addon_dir = Path(bpy.utils.script_path_user()) / "addons" / ADDON_NAME - _LOCALE_DIR = addon_dir / "locale" - _LOCALE_DIR.mkdir(exist_ok=True) - return _LOCALE_DIR - - def convert_translations_to_po(): - """extract current translation strings from translation.py to .po files and saved them in - both `locale` folder in addon's directory and I18n Branches directory""" + """extract current translation strings from translation.py to .po files + and saves them to I18n Branches directory""" - locale_dir = get_locale_dir() + temp_po_dir = tempfile.TemporaryDirectory() if not BRANCHES_DIR.is_dir(): raise Exception(f"I18n Branches directory doesn't exist: {BRANCHES_DIR.as_posix()}") - bpy.ops.ui.i18n_addon_translation_export( - module_name=ADDON_NAME, directory=locale_dir.as_posix(), use_export_pot=False - ) + bpy.ops.ui.i18n_addon_translation_export(module_name=ADDON_NAME, directory=temp_po_dir.name, use_export_pot=False) - # NOTE: we also setup I18n branches directory - # because it later will be used to edit translation from UI - for file in locale_dir.iterdir(): - if file.suffix != ".po": - continue + # NOTE: we use I18n branches directory + # because it is the directory that later will be used to edit translation from UI. + # 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 exported data that way + for file in Path(temp_po_dir.name).iterdir(): branches_subdir = BRANCHES_DIR / file.stem branches_subdir.mkdir(exist_ok=True) - shutil.copy(file, branches_subdir / file.name) + file.rename(branches_subdir / file.name) + + temp_po_dir.cleanup() def update_translations_from_po(): """load translation strings from po files at I18n Branches back to translations.py (they also get copied to `locale` directory of the addon) """ - locale_dir = get_locale_dir() + temp_po_dir = tempfile.TemporaryDirectory() + temp_po_dir_path = Path(temp_po_dir.name) for file in BRANCHES_DIR.glob("**/*"): if file.suffix != ".po": continue - shutil.copy(file, locale_dir / file.name) + shutil.copy(file, temp_po_dir_path / file.name) bpy.ops.ui.i18n_addon_translation_import( module_name=ADDON_NAME, - directory=locale_dir.as_posix(), + directory=temp_po_dir.name, ) + temp_po_dir.cleanup() + if __name__ == "__main__": if not is_addon_loaded("ui_translate"): From 8ff9bcef325f05c2b47c065e659e608c6254c2c1 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 29 Dec 2023 17:05:15 +0500 Subject: [PATCH 05/37] update translations in current blender session --- src/blenderbim/scripts/setup_translations.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/blenderbim/scripts/setup_translations.py b/src/blenderbim/scripts/setup_translations.py index e5d1c9bc48..e9b5374aca 100644 --- a/src/blenderbim/scripts/setup_translations.py +++ b/src/blenderbim/scripts/setup_translations.py @@ -2,6 +2,7 @@ import bpy import addon_utils import shutil import tempfile +import importlib from pathlib import Path context = bpy.context @@ -70,6 +71,13 @@ def update_translations_from_po(): temp_po_dir.cleanup() + # update translations in current Blender session + 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) + if __name__ == "__main__": if not is_addon_loaded("ui_translate"): From dbce57ffc5a941e4e63cd901b7e0bdd6e199c346 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 29 Dec 2023 17:20:39 +0500 Subject: [PATCH 06/37] setup_translations - original dump_py_messages as we're going to monkey patch it and this commit is needed to keep the history of changes --- src/blenderbim/scripts/setup_translations.py | 29 ++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/blenderbim/scripts/setup_translations.py b/src/blenderbim/scripts/setup_translations.py index e9b5374aca..69174546e9 100644 --- a/src/blenderbim/scripts/setup_translations.py +++ b/src/blenderbim/scripts/setup_translations.py @@ -3,6 +3,8 @@ import addon_utils import shutil import tempfile import importlib +import os +import bl_i18n_utils from pathlib import Path context = bpy.context @@ -79,6 +81,33 @@ def update_translations_from_po(): bpy.app.translations.register(ADDON_NAME, translations_module.translations_dict) +def dump_py_messages(msgs, reports, addons, settings, addons_only=False): + def _get_files(path): + if not os.path.exists(path): + return [] + if os.path.isdir(path): + return [os.path.join(dpath, fn) for dpath, _, fnames in os.walk(path) for fn in fnames + if fn.endswith(".py") and (fn == "__init__.py" + or not fn.startswith("_"))] + return [path] + + files = [] + if not addons_only: + for path in settings.CUSTOM_PY_UI_FILES: + for root in (bpy.utils.resource_path(t) for t in ("USER", "LOCAL", "SYSTEM")): + files += _get_files(os.path.join(root, path)) + + # Add all given addons. + for mod in addons: + fn = mod.__file__ + if os.path.basename(fn) == "__init__.py": + files += _get_files(os.path.dirname(fn)) + else: + files.append(fn) + + bl_i18n_utils.bl_extract_messages.dump_py_messages_from_files(msgs, reports, sorted(files), settings) + + if __name__ == "__main__": if not is_addon_loaded("ui_translate"): raise Exception('"Manage UI translations" addon is not enabled') From ff251b7a6a6b6b9302a6e7cbd71528c4d8af7f27 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 29 Dec 2023 17:24:17 +0500 Subject: [PATCH 07/37] monkey patch dump_py_messages to ignore blenderbim dependencies --- src/blenderbim/scripts/setup_translations.py | 34 ++++++++++++++++---- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/src/blenderbim/scripts/setup_translations.py b/src/blenderbim/scripts/setup_translations.py index 69174546e9..c074350fc6 100644 --- a/src/blenderbim/scripts/setup_translations.py +++ b/src/blenderbim/scripts/setup_translations.py @@ -81,14 +81,28 @@ def update_translations_from_po(): bpy.app.translations.register(ADDON_NAME, translations_module.translations_dict) -def dump_py_messages(msgs, reports, addons, settings, addons_only=False): - def _get_files(path): +def dump_py_messages_monkey_patch(msgs, reports, addons, settings, addons_only=False): + ignore_addon_dirs = ["libs"] + + def _get_files(path, ignore_dirs=tuple()): if not os.path.exists(path): return [] if os.path.isdir(path): - return [os.path.join(dpath, fn) for dpath, _, fnames in os.walk(path) for fn in fnames - if fn.endswith(".py") and (fn == "__init__.py" - or not fn.startswith("_"))] + files = [] + for dpath, subdirs, fnames in os.walk(path, topdown=True, followlinks=True): + if Path(dpath) in ignore_dirs: + subdirs.clear() # skip walking through `subdirs` + continue + + for fn in fnames: + if not fn.endswith(".py"): + continue + if fn.startswith("_") and fn != "__init__.py": + continue + files.append(os.path.join(dpath, fn)) + + return files + return [path] files = [] @@ -101,7 +115,9 @@ def dump_py_messages(msgs, reports, addons, settings, addons_only=False): for mod in addons: fn = mod.__file__ if os.path.basename(fn) == "__init__.py": - files += _get_files(os.path.dirname(fn)) + parent_dir = Path(fn).parent + ignore_dirs = [parent_dir / dpath for dpath in ignore_addon_dirs] + files += _get_files(os.path.dirname(fn), ignore_dirs=ignore_dirs) else: files.append(fn) @@ -122,6 +138,12 @@ if __name__ == "__main__": f" - {ui_translate_settings.BLENDER_I18N_PO_DIR}\n" ) + # we monkey patch `bl_i18n_utils.bl_extract_messages.dump_py_messages` + # as it's doesn't support ignoring folders + # and we need it, otherwise translation addon will try to parse strings + # from all BlenderBIM dependencies :O + bl_i18n_utils.bl_extract_messages.dump_py_messages = dump_py_messages_monkey_patch + # setup selected languages for lang in i18n_settings.langs: lang.use = lang.uid in SUPPORT_LANGUAGES From 1a09ce40e95b111d3054f57149ca5e7a2644ac65 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 9 Jan 2024 16:00:21 +0500 Subject: [PATCH 08/37] fix error converting translations to .po if .po files already exist --- src/blenderbim/scripts/setup_translations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blenderbim/scripts/setup_translations.py b/src/blenderbim/scripts/setup_translations.py index c074350fc6..1b4fce6d27 100644 --- a/src/blenderbim/scripts/setup_translations.py +++ b/src/blenderbim/scripts/setup_translations.py @@ -50,7 +50,7 @@ def convert_translations_to_po(): for file in Path(temp_po_dir.name).iterdir(): branches_subdir = BRANCHES_DIR / file.stem branches_subdir.mkdir(exist_ok=True) - file.rename(branches_subdir / file.name) + file.replace(branches_subdir / file.name) temp_po_dir.cleanup() From bfaa61e59254582aae1cdbd653bf7a59fce4fe59 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 9 Jan 2024 16:35:52 +0500 Subject: [PATCH 09/37] setup_translations - original dump_addon_messages same as with dump_py_messages - this commit is needed to track monkey patch changes later on --- src/blenderbim/scripts/setup_translations.py | 90 ++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/src/blenderbim/scripts/setup_translations.py b/src/blenderbim/scripts/setup_translations.py index 1b4fce6d27..18d4ced15c 100644 --- a/src/blenderbim/scripts/setup_translations.py +++ b/src/blenderbim/scripts/setup_translations.py @@ -124,6 +124,95 @@ def dump_py_messages_monkey_patch(msgs, reports, addons, settings, addons_only=F bl_i18n_utils.bl_extract_messages.dump_py_messages_from_files(msgs, reports, sorted(files), settings) +def dump_addon_messages(module_name, do_checks, settings): + import datetime + import addon_utils + import bl_i18n_utils.utils as utils + from bl_i18n_utils.bl_extract_messages import ( + _gen_reports, + _gen_check_ctxt, + dump_rna_messages, + _diff_check_ctxt, + dump_py_messages, + dump_addon_bl_info, + print_info, + ) + + # Get current addon state (loaded or not): + was_loaded = addon_utils.check(module_name)[1] + + # Enable our addon. + addon = utils.enable_addons(addons={module_name})[0] + + addon_info = addon_utils.module_bl_info(addon) + ver = addon_info["name"] + " " + ".".join(str(v) for v in addon_info["version"]) + rev = 0 + date = datetime.datetime.now() + pot = utils.I18nMessages.gen_empty_messages( + settings.PARSER_TEMPLATE_ID, ver, rev, date, date.year, settings=settings + ) + msgs = pot.msgs + + minus_pot = utils.I18nMessages.gen_empty_messages( + settings.PARSER_TEMPLATE_ID, ver, rev, date, date.year, settings=settings + ) + minus_msgs = minus_pot.msgs + + check_ctxt = _gen_check_ctxt(settings) if do_checks else None + minus_check_ctxt = _gen_check_ctxt(settings) if do_checks else None + + # Get strings from RNA, our addon being enabled. + print("A") + reports = _gen_reports(check_ctxt) + print("B") + dump_rna_messages(msgs, reports, settings) + print("C") + + # Now disable our addon, and re-scan RNA. + utils.enable_addons(addons={module_name}, disable=True) + print("D") + reports["check_ctxt"] = minus_check_ctxt + print("E") + dump_rna_messages(minus_msgs, reports, settings) + print("F") + + # Restore previous state if needed! + if was_loaded: + utils.enable_addons(addons={module_name}) + + # and make the diff! + for key in minus_msgs: + if key != settings.PO_HEADER_KEY: + if key in msgs: + del msgs[key] + else: + # This should not happen, but some messages seem to have + # leaked on add-on unregister and register? + print(f"Key not found in msgs: {key}") + + if check_ctxt: + _diff_check_ctxt(check_ctxt, minus_check_ctxt) + + # and we are done with those! + del minus_pot + del minus_msgs + del minus_check_ctxt + + # get strings from UI layout definitions text="..." args + reports["check_ctxt"] = check_ctxt + dump_py_messages(msgs, reports, {addon}, settings, addons_only=True) + + # Get strings from the addon's bl_info + dump_addon_bl_info(msgs, reports, addon, settings) + + pot.unescape() # Strings gathered in py/C source code may contain escaped chars... + print_info(reports, pot) + + print("Finished extracting UI messages!") + + return pot + + if __name__ == "__main__": if not is_addon_loaded("ui_translate"): raise Exception('"Manage UI translations" addon is not enabled') @@ -143,6 +232,7 @@ if __name__ == "__main__": # and we need it, otherwise translation addon will try to parse strings # from all BlenderBIM dependencies :O bl_i18n_utils.bl_extract_messages.dump_py_messages = dump_py_messages_monkey_patch + bl_i18n_utils.bl_extract_messages.dump_addon_messages = dump_addon_messages # setup selected languages for lang in i18n_settings.langs: From 6569d0031fbb4c67e8db174a8490224e09623097 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 9 Jan 2024 16:54:58 +0500 Subject: [PATCH 10/37] monkey patch blender's dump_addon_messages The way blender gather messages to translate is: 1) disable the addon 2) parse all available strings 3) enable the addon 4) parse all strings available strings again and subtract the ones gathered at step 2. But due Blender bug (https://projects.blender.org/blender/blender/issues/116579) not all addon parts unregistered at step 1, so some addon's strings left out and gathered at step 2 and then subtracted at step 4. At the end, we loose them and they will be missing from resulting translations files. I've monkey patched dump_addon_messages so it now works a bit more safe: 0) It expects addon to be disabled and Blender restarted after 1) Gather all Blender strings 2) Enable addon 3) Gather all Blender strings again and subtract strings from step 1. --- src/blenderbim/scripts/setup_translations.py | 30 +++++++------------- 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/src/blenderbim/scripts/setup_translations.py b/src/blenderbim/scripts/setup_translations.py index 18d4ced15c..8cb2ee0955 100644 --- a/src/blenderbim/scripts/setup_translations.py +++ b/src/blenderbim/scripts/setup_translations.py @@ -138,14 +138,8 @@ def dump_addon_messages(module_name, do_checks, settings): print_info, ) - # Get current addon state (loaded or not): - was_loaded = addon_utils.check(module_name)[1] - # Enable our addon. - addon = utils.enable_addons(addons={module_name})[0] - - addon_info = addon_utils.module_bl_info(addon) - ver = addon_info["name"] + " " + ".".join(str(v) for v in addon_info["version"]) + ver = module_name rev = 0 date = datetime.datetime.now() pot = utils.I18nMessages.gen_empty_messages( @@ -161,24 +155,20 @@ def dump_addon_messages(module_name, do_checks, settings): check_ctxt = _gen_check_ctxt(settings) if do_checks else None minus_check_ctxt = _gen_check_ctxt(settings) if do_checks else None - # Get strings from RNA, our addon being enabled. - print("A") - reports = _gen_reports(check_ctxt) - print("B") - dump_rna_messages(msgs, reports, settings) - print("C") - - # Now disable our addon, and re-scan RNA. - utils.enable_addons(addons={module_name}, disable=True) + # Get strings from RNA, our addon being disabled print("D") - reports["check_ctxt"] = minus_check_ctxt + reports = _gen_reports(check_ctxt) print("E") dump_rna_messages(minus_msgs, reports, settings) print("F") - # Restore previous state if needed! - if was_loaded: - utils.enable_addons(addons={module_name}) + # Now enable our addon, and re-scan RNA. + addon = utils.enable_addons(addons={module_name})[0] + print("A") + reports["check_ctxt"] = minus_check_ctxt + print("B") + dump_rna_messages(msgs, reports, settings) + print("C") # and make the diff! for key in minus_msgs: From f5736a7144e239af0e9cd9ee71f5ef2a7a58baf6 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 15 Jan 2024 10:55:12 +0500 Subject: [PATCH 11/37] convert setup_translations to blender addon for UI --- .../scripts/bbim_setup_translations.py | 315 ++++++++++++++++++ src/blenderbim/scripts/setup_translations.py | 234 ------------- 2 files changed, 315 insertions(+), 234 deletions(-) create mode 100644 src/blenderbim/scripts/bbim_setup_translations.py delete mode 100644 src/blenderbim/scripts/setup_translations.py diff --git a/src/blenderbim/scripts/bbim_setup_translations.py b/src/blenderbim/scripts/bbim_setup_translations.py new file mode 100644 index 0000000000..2826306a68 --- /dev/null +++ b/src/blenderbim/scripts/bbim_setup_translations.py @@ -0,0 +1,315 @@ +import bpy +import addon_utils +import shutil +import tempfile +import importlib +import os +import bl_i18n_utils +from pathlib import Path + +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", +} + +context = bpy.context +SUPPORT_LANGUAGES = ["ru_RU", "de_DE"] +ADDON_NAME = "localization_test" +BRANCHES_DIR = Path(context.preferences.filepaths.i18n_branches_directory) + + +def is_addon_loaded(addon_name): + loaded_default, loaded_state = addon_utils.check(addon_name) + return loaded_state + + +def dump_py_messages_monkey_patch(msgs, reports, addons, settings, addons_only=False): + ignore_addon_dirs = ["libs"] + + def _get_files(path, ignore_dirs=tuple()): + if not os.path.exists(path): + return [] + if os.path.isdir(path): + files = [] + for dpath, subdirs, fnames in os.walk(path, topdown=True, followlinks=True): + if Path(dpath) in ignore_dirs: + subdirs.clear() # skip walking through `subdirs` + continue + + for fn in fnames: + if not fn.endswith(".py"): + continue + if fn.startswith("_") and fn != "__init__.py": + continue + files.append(os.path.join(dpath, fn)) + + return files + + return [path] + + files = [] + if not addons_only: + for path in settings.CUSTOM_PY_UI_FILES: + for root in (bpy.utils.resource_path(t) for t in ("USER", "LOCAL", "SYSTEM")): + files += _get_files(os.path.join(root, path)) + + # Add all given addons. + for mod in addons: + fn = mod.__file__ + if os.path.basename(fn) == "__init__.py": + parent_dir = Path(fn).parent + ignore_dirs = [parent_dir / dpath for dpath in ignore_addon_dirs] + files += _get_files(os.path.dirname(fn), ignore_dirs=ignore_dirs) + else: + files.append(fn) + + bl_i18n_utils.bl_extract_messages.dump_py_messages_from_files(msgs, reports, sorted(files), settings) + + +def dump_addon_messages(module_name, do_checks, settings): + import datetime + import addon_utils + import bl_i18n_utils.utils as utils + from bl_i18n_utils.bl_extract_messages import ( + _gen_reports, + _gen_check_ctxt, + dump_rna_messages, + _diff_check_ctxt, + dump_py_messages, + dump_addon_bl_info, + print_info, + ) + + # Enable our addon. + ver = module_name + rev = 0 + date = datetime.datetime.now() + pot = utils.I18nMessages.gen_empty_messages( + settings.PARSER_TEMPLATE_ID, ver, rev, date, date.year, settings=settings + ) + msgs = pot.msgs + + minus_pot = utils.I18nMessages.gen_empty_messages( + settings.PARSER_TEMPLATE_ID, ver, rev, date, date.year, settings=settings + ) + minus_msgs = minus_pot.msgs + + check_ctxt = _gen_check_ctxt(settings) if do_checks else None + minus_check_ctxt = _gen_check_ctxt(settings) if do_checks else None + + # Get strings from RNA, our addon being disabled + print("D") + reports = _gen_reports(check_ctxt) + print("E") + dump_rna_messages(minus_msgs, reports, settings) + print("F") + + # Now enable our addon, and re-scan RNA. + addon = utils.enable_addons(addons={module_name})[0] + print("A") + reports["check_ctxt"] = minus_check_ctxt + print("B") + dump_rna_messages(msgs, reports, settings) + print("C") + + # and make the diff! + for key in minus_msgs: + if key != settings.PO_HEADER_KEY: + if key in msgs: + del msgs[key] + else: + # This should not happen, but some messages seem to have + # leaked on add-on unregister and register? + print(f"Key not found in msgs: {key}") + + if check_ctxt: + _diff_check_ctxt(check_ctxt, minus_check_ctxt) + + # and we are done with those! + del minus_pot + del minus_msgs + del minus_check_ctxt + + # get strings from UI layout definitions text="..." args + reports["check_ctxt"] = check_ctxt + dump_py_messages(msgs, reports, {addon}, settings, addons_only=True) + + # Get strings from the addon's bl_info + dump_addon_bl_info(msgs, reports, addon, settings) + + pot.unescape() # Strings gathered in py/C source code may contain escaped chars... + print_info(reports, pot) + + print("Finished extracting UI messages!") + + return pot + + +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"): + raise Exception('"Manage UI translations" addon is not enabled') + + from ui_translate.settings import settings as ui_translate_settings + + i18n_settings = context.window_manager.i18n_update_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" + ) + + # we monkey patch `bl_i18n_utils.bl_extract_messages.dump_py_messages` + # as it's doesn't support ignoring folders + # and we need it, otherwise translation addon will try to parse strings + # from all BlenderBIM dependencies :O + bl_i18n_utils.bl_extract_messages.dump_py_messages = dump_py_messages_monkey_patch + bl_i18n_utils.bl_extract_messages.dump_addon_messages = dump_addon_messages + + # setup selected languages + for lang in i18n_settings.langs: + lang.use = lang.uid in SUPPORT_LANGUAGES + + context.scene.translation_ui_is_loaded = True + + # TODO: restart blender + # https://blender.stackexchange.com/questions/282431/restarting-blender-with-a-script + + return {"FINISHED"} + + +class ReloadPyTranslations(bpy.types.Operator): + bl_idname = "bim.reload_py_translations" + bl_label = "Reload Py Translations" + bl_description = "Parse strings from Blender objects of the addon to `translations.py`" + bl_options = set() + + def execute(self, context): + if is_addon_loaded(ADDON_NAME): + # ref: https://projects.blender.org/blender/blender/issues/116579 + raise Exception( + f"'{ADDON_NAME}' addon is enabled.\n" + "Need to disable it, restart Blender and start reloading translations again.\n" + "Otherwise some strings to translate might get lost due Blender bug." + ) + + bpy.ops.ui.i18n_addon_translation_update("INVOKE_DEFAULT", module_name=ADDON_NAME) + self.report({"INFO"}, "Translations py data is saved.") + return {"FINISHED"} + + +class ConvertTranslationsToPo(bpy.types.Operator): + bl_idname = "bim.convert_translations_to_po" + bl_label = "Convert Translations To .po" + bl_description = ( + "Extract current translation strings from translation.py to .po files and saves them to I18n Branches directory" + ) + bl_options = set() + + def execute(self, context): + temp_po_dir = tempfile.TemporaryDirectory() + + if not BRANCHES_DIR.is_dir(): + raise Exception(f"I18n Branches directory doesn't exist: {BRANCHES_DIR.as_posix()}") + + bpy.ops.ui.i18n_addon_translation_export( + module_name=ADDON_NAME, directory=temp_po_dir.name, use_export_pot=False + ) + + # NOTE: we use I18n branches directory + # because it is the directory that later will be used to edit translation from UI. + # 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 exported data that way + for file in Path(temp_po_dir.name).iterdir(): + branches_subdir = BRANCHES_DIR / file.stem + branches_subdir.mkdir(exist_ok=True) + file.replace(branches_subdir / file.name) + + temp_po_dir.cleanup() + self.report({"INFO"}, f"Translations .po files are saved to {BRANCHES_DIR}.") + 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\n" + "back to translations.py (they also get copied to `locale` directory of the addon)" + ) + bl_options = set() + + def execute(self, context): + temp_po_dir = tempfile.TemporaryDirectory() + temp_po_dir_path = Path(temp_po_dir.name) + for file in BRANCHES_DIR.glob("**/*"): + if file.suffix != ".po": + continue + shutil.copy(file, temp_po_dir_path / file.name) + + bpy.ops.ui.i18n_addon_translation_import( + module_name=ADDON_NAME, + directory=temp_po_dir.name, + ) + + temp_po_dir.cleanup() + + # update translations in current Blender session + 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 + if not context.scene.translation_ui_is_loaded: + layout.operator("bim.setup_translation_ui") + return + + layout.operator("bim.reload_py_translations", icon="FILE_REFRESH") + layout.operator("bim.convert_translations_to_po", icon="EXPORT") + layout.operator("bim.update_translations_from_po", icon="IMPORT") + + +classes = ( + ReloadPyTranslations, + ConvertTranslationsToPo, + UpdateTranslationsFromPo, + SetupTranslationUI, + BBIM_PT_translations, +) + + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + bpy.types.Scene.translation_ui_is_loaded = bpy.props.BoolProperty(default=False) + + +def unregister(): + for cls in classes: + bpy.utils.unregister_class(cls) + del bpy.types.Scene.translation_ui_is_loaded diff --git a/src/blenderbim/scripts/setup_translations.py b/src/blenderbim/scripts/setup_translations.py deleted file mode 100644 index 8cb2ee0955..0000000000 --- a/src/blenderbim/scripts/setup_translations.py +++ /dev/null @@ -1,234 +0,0 @@ -import bpy -import addon_utils -import shutil -import tempfile -import importlib -import os -import bl_i18n_utils -from pathlib import Path - -context = bpy.context -SUPPORT_LANGUAGES = ["ru_RU", "de_DE"] -ADDON_NAME = "localization_test" -BRANCHES_DIR = Path(context.preferences.filepaths.i18n_branches_directory) - - -def is_addon_loaded(addon_name): - loaded_default, loaded_state = addon_utils.check(addon_name) - return loaded_state - - -def reload_translations(): - """Parse strings from Blender objects of the addon to `translations.py`""" - if is_addon_loaded(ADDON_NAME): - # ref: https://projects.blender.org/blender/blender/issues/116579 - raise Exception( - f"'{ADDON_NAME}' addon is enabled.\n" - "Need to disable it, restart Blender and start reloading translations again.\n" - "Otherwise some strings to translate might get lost due Blender bug." - ) - - bpy.ops.ui.i18n_addon_translation_update("INVOKE_DEFAULT", module_name=ADDON_NAME) - - -def convert_translations_to_po(): - """extract current translation strings from translation.py to .po files - and saves them to I18n Branches directory""" - - temp_po_dir = tempfile.TemporaryDirectory() - - if not BRANCHES_DIR.is_dir(): - raise Exception(f"I18n Branches directory doesn't exist: {BRANCHES_DIR.as_posix()}") - - bpy.ops.ui.i18n_addon_translation_export(module_name=ADDON_NAME, directory=temp_po_dir.name, use_export_pot=False) - - # NOTE: we use I18n branches directory - # because it is the directory that later will be used to edit translation from UI. - # 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 exported data that way - for file in Path(temp_po_dir.name).iterdir(): - branches_subdir = BRANCHES_DIR / file.stem - branches_subdir.mkdir(exist_ok=True) - file.replace(branches_subdir / file.name) - - temp_po_dir.cleanup() - - -def update_translations_from_po(): - """load translation strings from po files at I18n Branches - back to translations.py (they also get copied to `locale` directory of the addon) - """ - temp_po_dir = tempfile.TemporaryDirectory() - temp_po_dir_path = Path(temp_po_dir.name) - for file in BRANCHES_DIR.glob("**/*"): - if file.suffix != ".po": - continue - shutil.copy(file, temp_po_dir_path / file.name) - - bpy.ops.ui.i18n_addon_translation_import( - module_name=ADDON_NAME, - directory=temp_po_dir.name, - ) - - temp_po_dir.cleanup() - - # update translations in current Blender session - 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) - - -def dump_py_messages_monkey_patch(msgs, reports, addons, settings, addons_only=False): - ignore_addon_dirs = ["libs"] - - def _get_files(path, ignore_dirs=tuple()): - if not os.path.exists(path): - return [] - if os.path.isdir(path): - files = [] - for dpath, subdirs, fnames in os.walk(path, topdown=True, followlinks=True): - if Path(dpath) in ignore_dirs: - subdirs.clear() # skip walking through `subdirs` - continue - - for fn in fnames: - if not fn.endswith(".py"): - continue - if fn.startswith("_") and fn != "__init__.py": - continue - files.append(os.path.join(dpath, fn)) - - return files - - return [path] - - files = [] - if not addons_only: - for path in settings.CUSTOM_PY_UI_FILES: - for root in (bpy.utils.resource_path(t) for t in ("USER", "LOCAL", "SYSTEM")): - files += _get_files(os.path.join(root, path)) - - # Add all given addons. - for mod in addons: - fn = mod.__file__ - if os.path.basename(fn) == "__init__.py": - parent_dir = Path(fn).parent - ignore_dirs = [parent_dir / dpath for dpath in ignore_addon_dirs] - files += _get_files(os.path.dirname(fn), ignore_dirs=ignore_dirs) - else: - files.append(fn) - - bl_i18n_utils.bl_extract_messages.dump_py_messages_from_files(msgs, reports, sorted(files), settings) - - -def dump_addon_messages(module_name, do_checks, settings): - import datetime - import addon_utils - import bl_i18n_utils.utils as utils - from bl_i18n_utils.bl_extract_messages import ( - _gen_reports, - _gen_check_ctxt, - dump_rna_messages, - _diff_check_ctxt, - dump_py_messages, - dump_addon_bl_info, - print_info, - ) - - # Enable our addon. - ver = module_name - rev = 0 - date = datetime.datetime.now() - pot = utils.I18nMessages.gen_empty_messages( - settings.PARSER_TEMPLATE_ID, ver, rev, date, date.year, settings=settings - ) - msgs = pot.msgs - - minus_pot = utils.I18nMessages.gen_empty_messages( - settings.PARSER_TEMPLATE_ID, ver, rev, date, date.year, settings=settings - ) - minus_msgs = minus_pot.msgs - - check_ctxt = _gen_check_ctxt(settings) if do_checks else None - minus_check_ctxt = _gen_check_ctxt(settings) if do_checks else None - - # Get strings from RNA, our addon being disabled - print("D") - reports = _gen_reports(check_ctxt) - print("E") - dump_rna_messages(minus_msgs, reports, settings) - print("F") - - # Now enable our addon, and re-scan RNA. - addon = utils.enable_addons(addons={module_name})[0] - print("A") - reports["check_ctxt"] = minus_check_ctxt - print("B") - dump_rna_messages(msgs, reports, settings) - print("C") - - # and make the diff! - for key in minus_msgs: - if key != settings.PO_HEADER_KEY: - if key in msgs: - del msgs[key] - else: - # This should not happen, but some messages seem to have - # leaked on add-on unregister and register? - print(f"Key not found in msgs: {key}") - - if check_ctxt: - _diff_check_ctxt(check_ctxt, minus_check_ctxt) - - # and we are done with those! - del minus_pot - del minus_msgs - del minus_check_ctxt - - # get strings from UI layout definitions text="..." args - reports["check_ctxt"] = check_ctxt - dump_py_messages(msgs, reports, {addon}, settings, addons_only=True) - - # Get strings from the addon's bl_info - dump_addon_bl_info(msgs, reports, addon, settings) - - pot.unescape() # Strings gathered in py/C source code may contain escaped chars... - print_info(reports, pot) - - print("Finished extracting UI messages!") - - return pot - - -if __name__ == "__main__": - if not is_addon_loaded("ui_translate"): - raise Exception('"Manage UI translations" addon is not enabled') - - from ui_translate.settings import settings as ui_translate_settings - - i18n_settings = context.window_manager.i18n_update_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" - ) - - # we monkey patch `bl_i18n_utils.bl_extract_messages.dump_py_messages` - # as it's doesn't support ignoring folders - # and we need it, otherwise translation addon will try to parse strings - # from all BlenderBIM dependencies :O - bl_i18n_utils.bl_extract_messages.dump_py_messages = dump_py_messages_monkey_patch - bl_i18n_utils.bl_extract_messages.dump_addon_messages = dump_addon_messages - - # setup selected languages - for lang in i18n_settings.langs: - lang.use = lang.uid in SUPPORT_LANGUAGES - - # uncomment the action you want to perform: - # reload_translations() - # convert_translations_to_po() - # update_translations_from_po() From 770bc6ea61f7257b1daaa3f9325b1e0ebf9f3944 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 15 Jan 2024 12:03:48 +0500 Subject: [PATCH 12/37] Operator to disable BBIM and restart Blender as it's needed to reload the translations Update bbim_setup_translations.py Update bbim_setup_translations.py --- .../scripts/bbim_setup_translations.py | 35 ++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/src/blenderbim/scripts/bbim_setup_translations.py b/src/blenderbim/scripts/bbim_setup_translations.py index 2826306a68..24f5fa2e26 100644 --- a/src/blenderbim/scripts/bbim_setup_translations.py +++ b/src/blenderbim/scripts/bbim_setup_translations.py @@ -24,7 +24,7 @@ ADDON_NAME = "localization_test" BRANCHES_DIR = Path(context.preferences.filepaths.i18n_branches_directory) -def is_addon_loaded(addon_name): +def is_addon_loaded(addon_name) -> bool: loaded_default, loaded_state = addon_utils.check(addon_name) return loaded_state @@ -183,9 +183,6 @@ class SetupTranslationUI(bpy.types.Operator): context.scene.translation_ui_is_loaded = True - # TODO: restart blender - # https://blender.stackexchange.com/questions/282431/restarting-blender-with-a-script - return {"FINISHED"} @@ -276,6 +273,30 @@ class UpdateTranslationsFromPo(bpy.types.Operator): return {"FINISHED"} +class DisableEnableAddon(bpy.types.Operator): + bl_idname = "bim.disable_enable_addon" + bl_label = "Disable/Enable addon" + bl_description = "Will enable addon if it's disabled, will disable it and restart Blender otherwise" + bl_options = set() + + def execute(self, context): + if not is_addon_loaded(ADDON_NAME): + addon_utils.enable("blenderbim", default_set=True) + return {"FINISHED"} + + import os + import subprocess + + addon_utils.disable("blenderbim", default_set=True) + + blender_exe = bpy.app.binary_path + head, tail = os.path.split(blender_exe) + blender_launcher = os.path.join(head, "blender-launcher.exe") + subprocess.run([blender_launcher, "-con", "--python-expr", "import bpy; bpy.ops.wm.recover_last_session()"]) + bpy.ops.wm.quit_blender() + return {"FINISHED"} + + class BBIM_PT_translations(bpy.types.Panel): bl_label = "BlenderBIM Translations" bl_idname = "BBIM_PT_translations" @@ -290,6 +311,11 @@ class BBIM_PT_translations(bpy.types.Panel): return layout.operator("bim.reload_py_translations", icon="FILE_REFRESH") + addon_enabled = is_addon_loaded(ADDON_NAME) + layout.operator("bim.disable_enable_addon", + icon="QUIT" if addon_enabled else "PLUGIN", + text="Disable Addon And Restart Blender" if addon_enabled else "Enable Addon") + layout.separator() layout.operator("bim.convert_translations_to_po", icon="EXPORT") layout.operator("bim.update_translations_from_po", icon="IMPORT") @@ -299,6 +325,7 @@ classes = ( ConvertTranslationsToPo, UpdateTranslationsFromPo, SetupTranslationUI, + DisableEnableAddon, BBIM_PT_translations, ) From 2995b73c2d361e55386107c0ea4c0b24c7ed6a4e Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 15 Jan 2024 12:17:21 +0500 Subject: [PATCH 13/37] open .po directory operator --- src/blenderbim/scripts/bbim_setup_translations.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/blenderbim/scripts/bbim_setup_translations.py b/src/blenderbim/scripts/bbim_setup_translations.py index 24f5fa2e26..30d381a182 100644 --- a/src/blenderbim/scripts/bbim_setup_translations.py +++ b/src/blenderbim/scripts/bbim_setup_translations.py @@ -297,6 +297,18 @@ class DisableEnableAddon(bpy.types.Operator): return {"FINISHED"} +class OpenPoDirectory(bpy.types.Operator): + bl_idname = "bim.open_po_directory" + bl_label = "Open Directory With .po Files" + bl_options = set() + + def execute(self, context): + import webbrowser + + webbrowser.open(BRANCHES_DIR) + return {"FINISHED"} + + class BBIM_PT_translations(bpy.types.Panel): bl_label = "BlenderBIM Translations" bl_idname = "BBIM_PT_translations" @@ -316,6 +328,7 @@ class BBIM_PT_translations(bpy.types.Panel): icon="QUIT" if addon_enabled else "PLUGIN", text="Disable Addon And Restart Blender" if addon_enabled else "Enable Addon") layout.separator() + layout.operator("bim.open_po_directory", icon="FILE_FOLDER") layout.operator("bim.convert_translations_to_po", icon="EXPORT") layout.operator("bim.update_translations_from_po", icon="IMPORT") @@ -326,6 +339,7 @@ classes = ( UpdateTranslationsFromPo, SetupTranslationUI, DisableEnableAddon, + OpenPoDirectory, BBIM_PT_translations, ) From e106dc450295476d20a7639ad9cc3e34608a1571 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 15 Jan 2024 12:24:00 +0500 Subject: [PATCH 14/37] register translations dict only if addon is already loaded --- src/blenderbim/scripts/bbim_setup_translations.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/blenderbim/scripts/bbim_setup_translations.py b/src/blenderbim/scripts/bbim_setup_translations.py index 30d381a182..9735447e0b 100644 --- a/src/blenderbim/scripts/bbim_setup_translations.py +++ b/src/blenderbim/scripts/bbim_setup_translations.py @@ -264,11 +264,12 @@ class UpdateTranslationsFromPo(bpy.types.Operator): temp_po_dir.cleanup() # update translations in current Blender session - 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) + 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"} From 35223d968c050c02315e4135a9655834d175e948 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Mon, 15 Jan 2024 14:43:23 +0500 Subject: [PATCH 15/37] semi-automatic setup for necessary directories --- .../scripts/bbim_setup_translations.py | 67 ++++++++++++++----- 1 file changed, 52 insertions(+), 15 deletions(-) diff --git a/src/blenderbim/scripts/bbim_setup_translations.py b/src/blenderbim/scripts/bbim_setup_translations.py index 9735447e0b..2b352028ee 100644 --- a/src/blenderbim/scripts/bbim_setup_translations.py +++ b/src/blenderbim/scripts/bbim_setup_translations.py @@ -21,13 +21,13 @@ bl_info = { context = bpy.context SUPPORT_LANGUAGES = ["ru_RU", "de_DE"] ADDON_NAME = "localization_test" -BRANCHES_DIR = Path(context.preferences.filepaths.i18n_branches_directory) - 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(context.preferences.filepaths.i18n_branches_directory) def dump_py_messages_monkey_patch(msgs, reports, addons, settings, addons_only=False): ignore_addon_dirs = ["libs"] @@ -160,15 +160,52 @@ class SetupTranslationUI(bpy.types.Operator): if not is_addon_loaded("ui_translate"): raise Exception('"Manage UI translations" addon is not enabled') + addon_prefs = context.preferences.addons["ui_translate"].preferences + + # check branches directory + branches_dir = get_branches_directory() + if not branches_dir.is_dir(): + raise Exception(f"I18n Branches directory is not set up or doesn't exist ({branches_dir}). " + "Setup I18n branches directory (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 i18n_dir.is_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 source_dir.is_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: - 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" - ) + # 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" + ) # we monkey patch `bl_i18n_utils.bl_extract_messages.dump_py_messages` # as it's doesn't support ignoring folders @@ -216,9 +253,7 @@ class ConvertTranslationsToPo(bpy.types.Operator): def execute(self, context): temp_po_dir = tempfile.TemporaryDirectory() - - if not BRANCHES_DIR.is_dir(): - raise Exception(f"I18n Branches directory doesn't exist: {BRANCHES_DIR.as_posix()}") + branches_dir = get_branches_directory() bpy.ops.ui.i18n_addon_translation_export( module_name=ADDON_NAME, directory=temp_po_dir.name, use_export_pot=False @@ -230,12 +265,12 @@ class ConvertTranslationsToPo(bpy.types.Operator): # every .po file has a parent folder with the same name # so we rearrange the exported data that way for file in Path(temp_po_dir.name).iterdir(): - branches_subdir = BRANCHES_DIR / file.stem + branches_subdir = branches_dir / file.stem branches_subdir.mkdir(exist_ok=True) file.replace(branches_subdir / file.name) temp_po_dir.cleanup() - self.report({"INFO"}, f"Translations .po files are saved to {BRANCHES_DIR}.") + self.report({"INFO"}, f"Translations .po files are saved to {branches_dir}.") return {"FINISHED"} @@ -251,7 +286,9 @@ class UpdateTranslationsFromPo(bpy.types.Operator): def execute(self, context): temp_po_dir = tempfile.TemporaryDirectory() temp_po_dir_path = Path(temp_po_dir.name) - for file in BRANCHES_DIR.glob("**/*"): + branches_dir = get_branches_directory() + + for file in branches_dir.glob("**/*"): if file.suffix != ".po": continue shutil.copy(file, temp_po_dir_path / file.name) @@ -270,7 +307,7 @@ class UpdateTranslationsFromPo(bpy.types.Operator): 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}") + self.report({"INFO"}, f"Addon's translation updated from .po in {branches_dir}") return {"FINISHED"} @@ -306,7 +343,7 @@ class OpenPoDirectory(bpy.types.Operator): def execute(self, context): import webbrowser - webbrowser.open(BRANCHES_DIR) + webbrowser.open(get_branches_directory()) return {"FINISHED"} From 3546f1359379aa90a5d5da3f44c161bfd2b9d52e Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 16 Jan 2024 11:42:34 +0500 Subject: [PATCH 16/37] change test addon name from localization_test --- src/blenderbim/scripts/bbim_setup_translations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blenderbim/scripts/bbim_setup_translations.py b/src/blenderbim/scripts/bbim_setup_translations.py index 2b352028ee..15054f0a70 100644 --- a/src/blenderbim/scripts/bbim_setup_translations.py +++ b/src/blenderbim/scripts/bbim_setup_translations.py @@ -20,7 +20,7 @@ bl_info = { context = bpy.context SUPPORT_LANGUAGES = ["ru_RU", "de_DE"] -ADDON_NAME = "localization_test" +ADDON_NAME = "blenderbim" def is_addon_loaded(addon_name) -> bool: loaded_default, loaded_state = addon_utils.check(addon_name) From 382d3f1f9f32f52d9d8ec0eadace04439d76a69c Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 16 Jan 2024 15:04:02 +0500 Subject: [PATCH 17/37] generate pot file too converting translations.py --- src/blenderbim/scripts/bbim_setup_translations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blenderbim/scripts/bbim_setup_translations.py b/src/blenderbim/scripts/bbim_setup_translations.py index 15054f0a70..cad7b0c090 100644 --- a/src/blenderbim/scripts/bbim_setup_translations.py +++ b/src/blenderbim/scripts/bbim_setup_translations.py @@ -256,7 +256,7 @@ class ConvertTranslationsToPo(bpy.types.Operator): branches_dir = get_branches_directory() bpy.ops.ui.i18n_addon_translation_export( - module_name=ADDON_NAME, directory=temp_po_dir.name, use_export_pot=False + module_name=ADDON_NAME, directory=temp_po_dir.name, use_export_pot=True ) # NOTE: we use I18n branches directory From 764c3bbb8501c48d79a3111d9447c34e4a95bf99 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 16 Jan 2024 15:08:55 +0500 Subject: [PATCH 18/37] separate developer/translator ui --- src/blenderbim/scripts/bbim_setup_translations.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/blenderbim/scripts/bbim_setup_translations.py b/src/blenderbim/scripts/bbim_setup_translations.py index cad7b0c090..1120d60150 100644 --- a/src/blenderbim/scripts/bbim_setup_translations.py +++ b/src/blenderbim/scripts/bbim_setup_translations.py @@ -360,14 +360,16 @@ class BBIM_PT_translations(bpy.types.Panel): layout.operator("bim.setup_translation_ui") return + layout.label(text="Developer UI:") layout.operator("bim.reload_py_translations", icon="FILE_REFRESH") addon_enabled = is_addon_loaded(ADDON_NAME) layout.operator("bim.disable_enable_addon", icon="QUIT" if addon_enabled else "PLUGIN", text="Disable Addon And Restart Blender" if addon_enabled else "Enable Addon") - layout.separator() - layout.operator("bim.open_po_directory", icon="FILE_FOLDER") layout.operator("bim.convert_translations_to_po", icon="EXPORT") + layout.separator() + layout.label(text="Translator UI:") + layout.operator("bim.open_po_directory", icon="FILE_FOLDER") layout.operator("bim.update_translations_from_po", icon="IMPORT") From a88b7084423de9c9fc4c49b9bcc7fd5e6bc3d644 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 16 Jan 2024 15:15:11 +0500 Subject: [PATCH 19/37] fix issues with empty paths accepted as valid paths --- src/blenderbim/scripts/bbim_setup_translations.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/blenderbim/scripts/bbim_setup_translations.py b/src/blenderbim/scripts/bbim_setup_translations.py index 1120d60150..b450c7ce5f 100644 --- a/src/blenderbim/scripts/bbim_setup_translations.py +++ b/src/blenderbim/scripts/bbim_setup_translations.py @@ -162,9 +162,12 @@ class SetupTranslationUI(bpy.types.Operator): 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 branches_dir.is_dir(): + if not is_valid_path(branches_dir): raise Exception(f"I18n Branches directory is not set up or doesn't exist ({branches_dir}). " "Setup I18n branches directory (Preferences > File Paths > Development > I18n Branches) " "to a folder containing (or that will contain) .po files.") @@ -172,7 +175,7 @@ class SetupTranslationUI(bpy.types.Operator): # 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 i18n_dir.is_dir(): + 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}"') @@ -180,7 +183,7 @@ class SetupTranslationUI(bpy.types.Operator): 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 source_dir.is_dir(): + 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 From ed836f5b4a7f22817c52e720bf307d0b562599f5 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 16 Jan 2024 15:18:05 +0500 Subject: [PATCH 20/37] Update bbim_setup_translations.py --- src/blenderbim/scripts/bbim_setup_translations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blenderbim/scripts/bbim_setup_translations.py b/src/blenderbim/scripts/bbim_setup_translations.py index b450c7ce5f..d37b9f55b5 100644 --- a/src/blenderbim/scripts/bbim_setup_translations.py +++ b/src/blenderbim/scripts/bbim_setup_translations.py @@ -168,7 +168,7 @@ class SetupTranslationUI(bpy.types.Operator): # 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}). " + raise Exception(f"I18n Branches directory is not set up or doesn't exist ({branches_dir}).\n" "Setup I18n branches directory (Preferences > File Paths > Development > I18n Branches) " "to a folder containing (or that will contain) .po files.") From 30029e3dc71533e17f0783bf693ec3c5a95e7723 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 16 Jan 2024 15:24:03 +0500 Subject: [PATCH 21/37] expose i18n path in translator UI removed bim.open_po_directory operator as it's now available just from alt-clicking on the path browsing button --- .../scripts/bbim_setup_translations.py | 20 ++++--------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/src/blenderbim/scripts/bbim_setup_translations.py b/src/blenderbim/scripts/bbim_setup_translations.py index d37b9f55b5..7f2ee7b5df 100644 --- a/src/blenderbim/scripts/bbim_setup_translations.py +++ b/src/blenderbim/scripts/bbim_setup_translations.py @@ -169,7 +169,7 @@ class SetupTranslationUI(bpy.types.Operator): 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 (Preferences > File Paths > Development > I18n Branches) " + "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 @@ -338,18 +338,6 @@ class DisableEnableAddon(bpy.types.Operator): return {"FINISHED"} -class OpenPoDirectory(bpy.types.Operator): - bl_idname = "bim.open_po_directory" - bl_label = "Open Directory With .po Files" - bl_options = set() - - def execute(self, context): - import webbrowser - - webbrowser.open(get_branches_directory()) - return {"FINISHED"} - - class BBIM_PT_translations(bpy.types.Panel): bl_label = "BlenderBIM Translations" bl_idname = "BBIM_PT_translations" @@ -361,6 +349,7 @@ class BBIM_PT_translations(bpy.types.Panel): layout = self.layout if not context.scene.translation_ui_is_loaded: layout.operator("bim.setup_translation_ui") + layout.prop(context.preferences.filepaths, "i18n_branches_directory", text="I18n branches directory") return layout.label(text="Developer UI:") @@ -370,9 +359,9 @@ class BBIM_PT_translations(bpy.types.Panel): icon="QUIT" if addon_enabled else "PLUGIN", text="Disable Addon And Restart Blender" if addon_enabled else "Enable Addon") layout.operator("bim.convert_translations_to_po", icon="EXPORT") - layout.separator() + layout.separator(factor=3) layout.label(text="Translator UI:") - layout.operator("bim.open_po_directory", icon="FILE_FOLDER") + layout.prop(context.preferences.filepaths, "i18n_branches_directory") layout.operator("bim.update_translations_from_po", icon="IMPORT") @@ -382,7 +371,6 @@ classes = ( UpdateTranslationsFromPo, SetupTranslationUI, DisableEnableAddon, - OpenPoDirectory, BBIM_PT_translations, ) From f5f3edbc7968be67048c90882b240782c13410af Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 16 Jan 2024 15:27:43 +0500 Subject: [PATCH 22/37] check if translation ui is loaded by current Blender session instead of current Blender file as it's more reliable and thing could change after Blender restart --- src/blenderbim/scripts/bbim_setup_translations.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/blenderbim/scripts/bbim_setup_translations.py b/src/blenderbim/scripts/bbim_setup_translations.py index 7f2ee7b5df..c992b35b67 100644 --- a/src/blenderbim/scripts/bbim_setup_translations.py +++ b/src/blenderbim/scripts/bbim_setup_translations.py @@ -21,6 +21,7 @@ bl_info = { context = bpy.context SUPPORT_LANGUAGES = ["ru_RU", "de_DE"] ADDON_NAME = "blenderbim" +TRANSLATION_UI_IS_LOADED = False def is_addon_loaded(addon_name) -> bool: loaded_default, loaded_state = addon_utils.check(addon_name) @@ -221,7 +222,8 @@ class SetupTranslationUI(bpy.types.Operator): for lang in i18n_settings.langs: lang.use = lang.uid in SUPPORT_LANGUAGES - context.scene.translation_ui_is_loaded = True + global TRANSLATION_UI_IS_LOADED + TRANSLATION_UI_IS_LOADED = True return {"FINISHED"} @@ -347,7 +349,7 @@ class BBIM_PT_translations(bpy.types.Panel): def draw(self, context): layout = self.layout - if not context.scene.translation_ui_is_loaded: + if not TRANSLATION_UI_IS_LOADED: layout.operator("bim.setup_translation_ui") layout.prop(context.preferences.filepaths, "i18n_branches_directory", text="I18n branches directory") return @@ -378,10 +380,8 @@ classes = ( def register(): for cls in classes: bpy.utils.register_class(cls) - bpy.types.Scene.translation_ui_is_loaded = bpy.props.BoolProperty(default=False) def unregister(): for cls in classes: bpy.utils.unregister_class(cls) - del bpy.types.Scene.translation_ui_is_loaded From 04b8172ea90d9147469a8d070e6cea739aca669f Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 16 Jan 2024 15:28:55 +0500 Subject: [PATCH 23/37] black format --- .../scripts/bbim_setup_translations.py | 36 +++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/src/blenderbim/scripts/bbim_setup_translations.py b/src/blenderbim/scripts/bbim_setup_translations.py index c992b35b67..d49d087fe1 100644 --- a/src/blenderbim/scripts/bbim_setup_translations.py +++ b/src/blenderbim/scripts/bbim_setup_translations.py @@ -23,13 +23,16 @@ SUPPORT_LANGUAGES = ["ru_RU", "de_DE"] 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(context.preferences.filepaths.i18n_branches_directory) + def dump_py_messages_monkey_patch(msgs, reports, addons, settings, addons_only=False): ignore_addon_dirs = ["libs"] @@ -169,24 +172,32 @@ class SetupTranslationUI(bpy.types.Operator): # 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.") + 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}"') + 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]]) + 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}"') + 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" @@ -194,7 +205,10 @@ class SetupTranslationUI(bpy.types.Operator): # 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}.") + 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 @@ -357,9 +371,11 @@ class BBIM_PT_translations(bpy.types.Panel): layout.label(text="Developer UI:") layout.operator("bim.reload_py_translations", icon="FILE_REFRESH") addon_enabled = is_addon_loaded(ADDON_NAME) - layout.operator("bim.disable_enable_addon", - icon="QUIT" if addon_enabled else "PLUGIN", - text="Disable Addon And Restart Blender" if addon_enabled else "Enable Addon") + layout.operator( + "bim.disable_enable_addon", + icon="QUIT" if addon_enabled else "PLUGIN", + text="Disable Addon And Restart Blender" if addon_enabled else "Enable Addon", + ) layout.operator("bim.convert_translations_to_po", icon="EXPORT") layout.separator(factor=3) layout.label(text="Translator UI:") From 1112b102f75033a07d9446fba96d8c194f700036 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 16 Jan 2024 17:06:18 +0500 Subject: [PATCH 24/37] add without-blender-way to generate translations module from po files though it does require `bpy` module which is available only for python 3.10. Actually, `bpy` module by itself is not necessary - we just need `bl_i18n_utils` that becomes available after `import `bpy`. So alternatively we can just download that module from Blender https://projects.blender.org/blender/blender/src/branch/main/scripts/modules/bl_i18n_utils --- .../scripts/bbim_setup_translations.py | 61 ++++++++++++++++--- 1 file changed, 53 insertions(+), 8 deletions(-) diff --git a/src/blenderbim/scripts/bbim_setup_translations.py b/src/blenderbim/scripts/bbim_setup_translations.py index d49d087fe1..9a6575ff6a 100644 --- a/src/blenderbim/scripts/bbim_setup_translations.py +++ b/src/blenderbim/scripts/bbim_setup_translations.py @@ -4,6 +4,7 @@ import shutil import tempfile import importlib import os +import sys import bl_i18n_utils from pathlib import Path @@ -33,6 +34,20 @@ def get_branches_directory(): return Path(context.preferences.filepaths.i18n_branches_directory) +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) + + def dump_py_messages_monkey_patch(msgs, reports, addons, settings, addons_only=False): ignore_addon_dirs = ["libs"] @@ -304,21 +319,14 @@ class UpdateTranslationsFromPo(bpy.types.Operator): def execute(self, context): temp_po_dir = tempfile.TemporaryDirectory() - temp_po_dir_path = Path(temp_po_dir.name) branches_dir = get_branches_directory() - - for file in branches_dir.glob("**/*"): - if file.suffix != ".po": - continue - shutil.copy(file, temp_po_dir_path / file.name) + rearrange_files_for_po_import(branches_dir, temp_po_dir) bpy.ops.ui.i18n_addon_translation_import( module_name=ADDON_NAME, directory=temp_po_dir.name, ) - temp_po_dir.cleanup() - # update translations in current Blender session if is_addon_loaded(ADDON_NAME): bpy.app.translations.unregister(ADDON_NAME) @@ -401,3 +409,40 @@ def register(): def unregister(): for cls in classes: bpy.utils.unregister_class(cls) + + +def update_translations_from_po(po_directory: Path, translations_module: Path): + from bl_i18n_utils.settings import I18nSettings + import bl_i18n_utils.utils as utils_i18n + + temp_po_dir = tempfile.TemporaryDirectory() + rearrange_files_for_po_import(po_directory, temp_po_dir) + + settings = I18nSettings() + + trans = utils_i18n.I18n(kind="PY", src=translations_module.as_posix(), settings=settings) + po_files = dict(utils_i18n.get_po_files_from_dir(temp_po_dir.name)) + + for po_uid, po_filepath in po_files.items(): + po_uid = po_uid[0] + msgs = utils_i18n.I18nMessages(uid=po_uid, kind="PO", key=po_uid, src=po_filepath, settings=settings) + if po_uid in trans.trans: + trans.trans[po_uid].merge(msgs, replace=True) + else: + trans.trans[po_uid] = msgs + + trans.write(kind="PY") + + +if __name__ == "__main__": + # Example: + # py src/blenderbim/scripts/bbim_setup_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)) From ba6ba29da8fd997e4c491674e4cb973c8d952b0e Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 16 Jan 2024 17:34:24 +0500 Subject: [PATCH 25/37] auto enable "Manage UI Translations" addon if it's not enabled --- src/blenderbim/scripts/bbim_setup_translations.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/blenderbim/scripts/bbim_setup_translations.py b/src/blenderbim/scripts/bbim_setup_translations.py index 9a6575ff6a..2c413b0475 100644 --- a/src/blenderbim/scripts/bbim_setup_translations.py +++ b/src/blenderbim/scripts/bbim_setup_translations.py @@ -177,7 +177,8 @@ class SetupTranslationUI(bpy.types.Operator): def execute(self, context): if not is_addon_loaded("ui_translate"): - raise Exception('"Manage UI translations" addon is not enabled') + 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 From 98b56aea42f8082b0afd77474fcf26998ef4e205 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 17 Jan 2024 12:07:02 +0500 Subject: [PATCH 26/37] allow missing translations.py as not everyone will need translations for BBIM --- src/blenderbim/blenderbim/bim/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/blenderbim/blenderbim/bim/__init__.py b/src/blenderbim/blenderbim/bim/__init__.py index 64fbca60d3..a430ab2775 100644 --- a/src/blenderbim/blenderbim/bim/__init__.py +++ b/src/blenderbim/blenderbim/bim/__init__.py @@ -22,9 +22,12 @@ import bpy import bpy.utils.previews import blenderbim import importlib -from blenderbim.translations import translations_dict 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 = { From e3f5d3d2b77cad9397181ed5ae032bdab7e07282 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 17 Jan 2024 12:08:06 +0500 Subject: [PATCH 27/37] rename BlenderBIM translation script/addon --- .../{bbim_setup_translations.py => bbim_translations.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/blenderbim/scripts/{bbim_setup_translations.py => bbim_translations.py} (99%) diff --git a/src/blenderbim/scripts/bbim_setup_translations.py b/src/blenderbim/scripts/bbim_translations.py similarity index 99% rename from src/blenderbim/scripts/bbim_setup_translations.py rename to src/blenderbim/scripts/bbim_translations.py index 2c413b0475..67b6571383 100644 --- a/src/blenderbim/scripts/bbim_setup_translations.py +++ b/src/blenderbim/scripts/bbim_translations.py @@ -437,7 +437,7 @@ def update_translations_from_po(po_directory: Path, translations_module: Path): if __name__ == "__main__": # Example: - # py src/blenderbim/scripts/bbim_setup_translations.py -i "C:/blenderbim-translations" -o "C:/Blender/4.0/scripts/addons/blenderbim/translations.py" + # 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") From aecc3d56b88641a14aaaad015a46075c468e5287 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 17 Jan 2024 15:20:51 +0500 Subject: [PATCH 28/37] switch to our own string parser instead of blender's --- src/blenderbim/scripts/bbim_translations.py | 89 ++++++++++++++++++--- 1 file changed, 80 insertions(+), 9 deletions(-) diff --git a/src/blenderbim/scripts/bbim_translations.py b/src/blenderbim/scripts/bbim_translations.py index 67b6571383..b814f2a53b 100644 --- a/src/blenderbim/scripts/bbim_translations.py +++ b/src/blenderbim/scripts/bbim_translations.py @@ -5,6 +5,7 @@ import tempfile import importlib import os import sys +import re import bl_i18n_utils from pathlib import Path @@ -48,6 +49,65 @@ def rearrange_files_for_po_import(po_dir_path: Path, temp_directory: tempfile.Te shutil.copy(file, temp_po_dir_path / file.name) +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_module = importlib.import_module(ADDON_NAME) + addon_directory = Path(addon_module.__file__).parent + 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 + ] + regexes = [re.compile(pattern) for pattern in patterns] + operator_matches = set() + matches = set() + + for root, dirs, files in os.walk(directory): + for file in files: + if not file.endswith(".py"): + continue + filepath = os.path.join(root, file) + with open(filepath, "r", encoding="utf-8") as f: + content = f.read() + for i, regex in enumerate(regexes): + is_operator = i < 2 + for match in regex.finditer(content): + if "{" in match.group(1): + # Includes a formatting string. To fix. + # print(match.group(1)) + pass + elif is_operator: + operator_matches.add(match.group(1)) + else: + matches.add(match.group(1)) + + pot_filepath = po_directory / "blenderbim.pot" + with open(pot_filepath, "w", encoding="utf-8") as fo: + for m in sorted(operator_matches): + fo.write('msgctxt "Operator"\n') + fo.write(f'msgid "{m}"\n') + fo.write('msgstr ""\n') + fo.write("\n") + + for m in sorted(matches): + fo.write(f'msgid "{m}"\n') + fo.write('msgstr ""\n') + fo.write("\n") + + def dump_py_messages_monkey_patch(msgs, reports, addons, settings, addons_only=False): ignore_addon_dirs = ["libs"] @@ -263,17 +323,23 @@ class ReloadPyTranslations(bpy.types.Operator): bl_label = "Reload Py Translations" bl_description = "Parse strings from Blender objects of the addon to `translations.py`" bl_options = set() + use_bbim_parser: bpy.props.BoolProperty( + name="Use BlenderBIM Parser", description="As oppose to Blender parser", default=True + ) def execute(self, context): - if is_addon_loaded(ADDON_NAME): - # ref: https://projects.blender.org/blender/blender/issues/116579 - raise Exception( - f"'{ADDON_NAME}' addon is enabled.\n" - "Need to disable it, restart Blender and start reloading translations again.\n" - "Otherwise some strings to translate might get lost due Blender bug." - ) + if self.use_bbim_parser: + blenderbim_strings_parse() + else: + if is_addon_loaded(ADDON_NAME): + # ref: https://projects.blender.org/blender/blender/issues/116579 + raise Exception( + f"'{ADDON_NAME}' addon is enabled.\n" + "Need to disable it, restart Blender and start reloading translations again.\n" + "Otherwise some strings to translate might get lost due Blender bug." + ) - bpy.ops.ui.i18n_addon_translation_update("INVOKE_DEFAULT", module_name=ADDON_NAME) + bpy.ops.ui.i18n_addon_translation_update("INVOKE_DEFAULT", module_name=ADDON_NAME) self.report({"INFO"}, "Translations py data is saved.") return {"FINISHED"} @@ -379,12 +445,17 @@ class BBIM_PT_translations(bpy.types.Panel): layout.label(text="Developer UI:") layout.operator("bim.reload_py_translations", icon="FILE_REFRESH") + + # blender restart is disabled as we'll parse strings with BBIM parser addon_enabled = is_addon_loaded(ADDON_NAME) - layout.operator( + row = layout.row() + row.operator( "bim.disable_enable_addon", icon="QUIT" if addon_enabled else "PLUGIN", text="Disable Addon And Restart Blender" if addon_enabled else "Enable Addon", ) + row.enabled = False + layout.operator("bim.convert_translations_to_po", icon="EXPORT") layout.separator(factor=3) layout.label(text="Translator UI:") From e867602d9d8d14a5e802bcbb8aae0ae1af179174 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 17 Jan 2024 16:06:50 +0500 Subject: [PATCH 29/37] keep order of strings in .pot as they were parsed and store source filepaths --- src/blenderbim/scripts/bbim_translations.py | 56 +++++++++++++++------ 1 file changed, 40 insertions(+), 16 deletions(-) diff --git a/src/blenderbim/scripts/bbim_translations.py b/src/blenderbim/scripts/bbim_translations.py index b814f2a53b..104ff1fb40 100644 --- a/src/blenderbim/scripts/bbim_translations.py +++ b/src/blenderbim/scripts/bbim_translations.py @@ -8,6 +8,8 @@ import sys import re import bl_i18n_utils from pathlib import Path +from dataclasses import dataclass +from typing import Dict bl_info = { "name": "BlenderBIM Translations", @@ -49,6 +51,13 @@ def rearrange_files_for_po_import(po_dir_path: Path, temp_directory: tempfile.Te shutil.copy(file, temp_po_dir_path / file.name) +@dataclass +class Message: + msg_id: str + context: str | None + sources: list[str] + + 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 @@ -72,38 +81,53 @@ def blenderbim_strings_parse(addon_directory=None, po_directory=None): r'info\(\{.*?\}\s*,\s*"(.*?)"', # operator info ] regexes = [re.compile(pattern) for pattern in patterns] - operator_matches = set() - matches = set() + 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 = os.path.join(root, file) + 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 match in regex.finditer(content): - if "{" in match.group(1): + 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)) - pass + continue elif is_operator: - operator_matches.add(match.group(1)) + ctx = "Operator" else: - matches.add(match.group(1)) + ctx = None + message = matched_dict.get(string) + if message is None: + message = Message(string, ctx, []) + matched_dict[string] = message + elif ctx != message.context 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 m in sorted(operator_matches): - fo.write('msgctxt "Operator"\n') - fo.write(f'msgid "{m}"\n') - fo.write('msgstr ""\n') - fo.write("\n") - - for m in sorted(matches): - fo.write(f'msgid "{m}"\n') + 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.context != None: + fo.write(f'msgctxt "{msg.context}"\n') + fo.write(f'msgid "{msg.msg_id}"\n') fo.write('msgstr ""\n') fo.write("\n") From 9e14829cc1efd2fedccb9d5fcc487f53ba0e6261 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 17 Jan 2024 16:24:22 +0500 Subject: [PATCH 30/37] bbim messages parser to also parse gettext --- src/blenderbim/blenderbim/bim/module/style/prop.py | 10 +++++++--- src/blenderbim/scripts/bbim_translations.py | 1 + 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/blenderbim/blenderbim/bim/module/style/prop.py b/src/blenderbim/blenderbim/bim/module/style/prop.py index d7e73f4f3e..965165c363 100644 --- a/src/blenderbim/blenderbim/bim/module/style/prop.py +++ b/src/blenderbim/blenderbim/bim/module/style/prop.py @@ -32,6 +32,10 @@ from bpy.props import ( CollectionProperty, ) +import 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")), ] diff --git a/src/blenderbim/scripts/bbim_translations.py b/src/blenderbim/scripts/bbim_translations.py index 104ff1fb40..ea2598ed73 100644 --- a/src/blenderbim/scripts/bbim_translations.py +++ b/src/blenderbim/scripts/bbim_translations.py @@ -79,6 +79,7 @@ def blenderbim_strings_parse(addon_directory=None, po_directory=None): 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() From e5fe039fd080c49ac4bdb6f39f12cc7307f36b40 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 17 Jan 2024 16:27:37 +0500 Subject: [PATCH 31/37] expose current blender locale in bbim translations ui --- src/blenderbim/scripts/bbim_translations.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/blenderbim/scripts/bbim_translations.py b/src/blenderbim/scripts/bbim_translations.py index ea2598ed73..32cb6f3754 100644 --- a/src/blenderbim/scripts/bbim_translations.py +++ b/src/blenderbim/scripts/bbim_translations.py @@ -23,7 +23,9 @@ bl_info = { } context = bpy.context -SUPPORT_LANGUAGES = ["ru_RU", "de_DE"] +# 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 @@ -335,7 +337,7 @@ class SetupTranslationUI(bpy.types.Operator): # setup selected languages for lang in i18n_settings.langs: - lang.use = lang.uid in SUPPORT_LANGUAGES + lang.use = lang.uid == context.preferences.view.language global TRANSLATION_UI_IS_LOADED TRANSLATION_UI_IS_LOADED = True @@ -463,6 +465,8 @@ class BBIM_PT_translations(bpy.types.Panel): def draw(self, context): layout = self.layout + layout.prop(context.preferences.view, "language") + if not TRANSLATION_UI_IS_LOADED: layout.operator("bim.setup_translation_ui") layout.prop(context.preferences.filepaths, "i18n_branches_directory", text="I18n branches directory") From 2d279b7a7fc746ff55102688192bc837b02f481f Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Wed, 17 Jan 2024 17:10:07 +0500 Subject: [PATCH 32/37] update blenderbim build to include translation data --- src/blenderbim/Makefile | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/blenderbim/Makefile b/src/blenderbim/Makefile index 78a71de230..c69423b7fb 100644 --- a/src/blenderbim/Makefile +++ b/src/blenderbim/Makefile @@ -661,6 +661,13 @@ endif # Required for Desktop icon and file association cp -r blenderbim/libs/desktop dist/blenderbim/libs/ + # generate translations module for BBIM build + # note that bpy is typically available only for current Blender python version + pip install bpy + git clone https://github.com/IfcOpenShell/blenderbim-translations.git dist/working + python blenderbim/scripts/bbim_translations.py -i "dist/working" -o "dist/blenderbim/translations.py" + 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 From 9e46bacc12527957ec04896f459c7cfd7d54f74a Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 19 Jan 2024 12:06:11 +0500 Subject: [PATCH 33/37] gettext bug fix --- src/blenderbim/blenderbim/bim/module/style/prop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/blenderbim/blenderbim/bim/module/style/prop.py b/src/blenderbim/blenderbim/bim/module/style/prop.py index 965165c363..cf17b2ace2 100644 --- a/src/blenderbim/blenderbim/bim/module/style/prop.py +++ b/src/blenderbim/blenderbim/bim/module/style/prop.py @@ -34,7 +34,7 @@ from bpy.props import ( import gettext -_ = gettext +_ = gettext.gettext def get_style_types(self, context): From a30a796c07c1dfb4a16ee7cb275aff5500188091 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 19 Jan 2024 12:16:40 +0500 Subject: [PATCH 34/37] generate translations.py without bpy --- src/blenderbim/scripts/bbim_translations.py | 157 +++++++++++++++++--- 1 file changed, 134 insertions(+), 23 deletions(-) diff --git a/src/blenderbim/scripts/bbim_translations.py b/src/blenderbim/scripts/bbim_translations.py index 32cb6f3754..ae530df1ac 100644 --- a/src/blenderbim/scripts/bbim_translations.py +++ b/src/blenderbim/scripts/bbim_translations.py @@ -39,6 +39,11 @@ def get_branches_directory(): return Path(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. @@ -55,9 +60,11 @@ def rearrange_files_for_po_import(po_dir_path: Path, temp_directory: tempfile.Te @dataclass class Message: - msg_id: str - context: str | None + msgid: str + msgctxt: str | None sources: list[str] + # mapping languages to translated strings + translations: Dict[str, str] def blenderbim_strings_parse(addon_directory=None, po_directory=None): @@ -67,8 +74,7 @@ def blenderbim_strings_parse(addon_directory=None, po_directory=None): # ref: https://projects.blender.org/blender/blender/issues/116579 if addon_directory is None: - addon_module = importlib.import_module(ADDON_NAME) - addon_directory = Path(addon_module.__file__).parent + addon_directory = get_addon_directory() directory = addon_directory / "bim" if po_directory is None: @@ -81,7 +87,7 @@ def blenderbim_strings_parse(addon_directory=None, po_directory=None): r'Property\(.*?name\s*=\s*"(.*?)"', # property names r'report\(\{.*?\}\s*,\s*"(.*?)"', # operator reports r'info\(\{.*?\}\s*,\s*"(.*?)"', # operator info - r'\b_\("(.*?)"\)' # gettext called with `_` + r'\b_\("(.*?)"\)', # gettext called with `_` ] regexes = [re.compile(pattern) for pattern in patterns] matched_dict: Dict[str, Message] = dict() @@ -114,7 +120,7 @@ def blenderbim_strings_parse(addon_directory=None, po_directory=None): if message is None: message = Message(string, ctx, []) matched_dict[string] = message - elif ctx != message.context and False: + 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}" @@ -128,13 +134,112 @@ def blenderbim_strings_parse(addon_directory=None, po_directory=None): sources = list(dict.fromkeys(msg.sources)) for source in sources: fo.write(f"#: {source}\n") - if msg.context != None: - fo.write(f'msgctxt "{msg.context}"\n') - fo.write(f'msgid "{msg.msg_id}"\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 + for po_file_path in po_directory.glob("**/*.po"): + lang = po_file_path.stem + 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 + # code originating from Blender's bl_i18n_utils/utils.py + # https://projects.blender.org/blender/blender/src/branch/main/scripts/modules/bl_i18n_utils/utils.py + ret = [ + "# Tuple of tuples:", + "# ((msgctxt, msgid), (sources, gen_comments), (lang, translation, (is_fuzzy, comments)), ...)", + "translations_tuple = (", + ] + tab = " " + default_context = "*" + for msgid, msg in translation_data.items(): + # Key (context + msgid). + msgctxt = msg.msgctxt + if not msgctxt: + msgctxt = default_context + ret.append(tab + '(({}, "{}"),'.format(f'"{msgctxt}"' if msgctxt else "None", msgid)) + # Common comments (mostly sources!). + sources = [] + if not (sources): + ret.append(tab + " ((), ()),") + else: + if len(sources) > 1: + # make sure sources are unique but keep the order + sources = list(dict.fromkeys(msg.sources)) + ret.append(tab + f' (("{sources[0]}",') + ret += [tab + f' "{s}",' for s in sources[1:-1]] + ret.append(tab + f' "{sources[-1]}"),') + else: + ret.append(tab + " ((" + (f'"{sources[0]}",' if sources else "") + "),") + + # All languages + for lang, msgstr in msg.translations.items(): + is_fuzzy = False + # Language code and translation. + ret.append(tab + ' ("' + lang + f'", "{msgstr}",') + # User comments and fuzzy. + comments = [] + lngspaces = " " * (len(lang) + 6) + ret.append(tab + lngspaces + "(" + ("True" if is_fuzzy else "False") + ",") + ret[-1] = ret[-1] + " (" + ((f'"{comments[0]}",') if comments else "") + ")))," + + ret.append(tab + "),") + + ret += [ + ")", + "", + "translations_dict = {}", + "for msg in translations_tuple:", + tab + "key = msg[0]", + tab + "for lang, trans, (is_fuzzy, comments) in msg[2:]:", + tab * 2 + "if trans and not is_fuzzy:", + tab * 3 + "translations_dict.setdefault(lang, {})[key] = trans", + "", + ] + + with open(translations_module / "translations.py", "w") as fo: + fo.write("\n".join(ret)) + + def dump_py_messages_monkey_patch(msgs, reports, addons, settings, addons_only=False): ignore_addon_dirs = ["libs"] @@ -409,25 +514,31 @@ class UpdateTranslationsFromPo(bpy.types.Operator): "Load translation strings from po files at I18n Branches\n" "back to translations.py (they also get copied to `locale` directory of the addon)" ) + use_bbim_parser: bpy.props.BoolProperty( + name="Use BlenderBIM Parser", description="As oppose to Blender parser", default=True + ) bl_options = set() def execute(self, context): - temp_po_dir = tempfile.TemporaryDirectory() branches_dir = get_branches_directory() - rearrange_files_for_po_import(branches_dir, temp_po_dir) + if self.use_bbim_parser: + update_translations_from_po(branches_dir, get_addon_directory()) + else: + temp_po_dir = tempfile.TemporaryDirectory() + rearrange_files_for_po_import(branches_dir, temp_po_dir) - bpy.ops.ui.i18n_addon_translation_import( - module_name=ADDON_NAME, - directory=temp_po_dir.name, - ) + bpy.ops.ui.i18n_addon_translation_import( + module_name=ADDON_NAME, + directory=temp_po_dir.name, + ) - # update translations in current Blender session - 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) + # update translations in current Blender session + 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"} @@ -512,7 +623,7 @@ def unregister(): bpy.utils.unregister_class(cls) -def update_translations_from_po(po_directory: Path, translations_module: Path): +def bpy_update_translations_from_po(po_directory: Path, translations_module: Path): from bl_i18n_utils.settings import I18nSettings import bl_i18n_utils.utils as utils_i18n From eb971d3d731874b18cfc55d297dc0c9d99f4bedb Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 19 Jan 2024 13:57:19 +0500 Subject: [PATCH 35/37] save just a final dictionary to translations.py instead of tuples We can do that since In our workflow we won't be converting translations.py to .po files, that way it will be much shorter as it's containing only translated strings and doesn't have strings sources. --- src/blenderbim/scripts/bbim_translations.py | 63 +++++---------------- 1 file changed, 14 insertions(+), 49 deletions(-) diff --git a/src/blenderbim/scripts/bbim_translations.py b/src/blenderbim/scripts/bbim_translations.py index ae530df1ac..a7b371ad70 100644 --- a/src/blenderbim/scripts/bbim_translations.py +++ b/src/blenderbim/scripts/bbim_translations.py @@ -171,8 +171,10 @@ def update_translations_from_po(po_directory: Path, translations_module: Path): 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: @@ -182,59 +184,22 @@ def update_translations_from_po(po_directory: Path, translations_module: Path): current_chunk = [] # generate translations.py file - # code originating from Blender's bl_i18n_utils/utils.py - # https://projects.blender.org/blender/blender/src/branch/main/scripts/modules/bl_i18n_utils/utils.py - ret = [ - "# Tuple of tuples:", - "# ((msgctxt, msgid), (sources, gen_comments), (lang, translation, (is_fuzzy, comments)), ...)", - "translations_tuple = (", - ] tab = " " default_context = "*" - for msgid, msg in translation_data.items(): - # Key (context + msgid). - msgctxt = msg.msgctxt - if not msgctxt: - msgctxt = default_context - ret.append(tab + '(({}, "{}"),'.format(f'"{msgctxt}"' if msgctxt else "None", msgid)) - # Common comments (mostly sources!). - sources = [] - if not (sources): - ret.append(tab + " ((), ()),") - else: - if len(sources) > 1: - # make sure sources are unique but keep the order - sources = list(dict.fromkeys(msg.sources)) - ret.append(tab + f' (("{sources[0]}",') - ret += [tab + f' "{s}",' for s in sources[1:-1]] - ret.append(tab + f' "{sources[-1]}"),') - else: - ret.append(tab + " ((" + (f'"{sources[0]}",' if sources else "") + "),") + ret = ["translations_dict = {"] - # All languages - for lang, msgstr in msg.translations.items(): - is_fuzzy = False - # Language code and translation. - ret.append(tab + ' ("' + lang + f'", "{msgstr}",') - # User comments and fuzzy. - comments = [] - lngspaces = " " * (len(lang) + 6) - ret.append(tab + lngspaces + "(" + ("True" if is_fuzzy else "False") + ",") - ret[-1] = ret[-1] + " (" + ((f'"{comments[0]}",') if comments else "") + ")))," + 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(tab + "),") - - ret += [ - ")", - "", - "translations_dict = {}", - "for msg in translations_tuple:", - tab + "key = msg[0]", - tab + "for lang, trans, (is_fuzzy, comments) in msg[2:]:", - tab * 2 + "if trans and not is_fuzzy:", - tab * 3 + "translations_dict.setdefault(lang, {})[key] = trans", - "", - ] + ret.append("}") with open(translations_module / "translations.py", "w") as fo: fo.write("\n".join(ret)) From 66fa1cedd9d205a7033573321a05a9aa5b51c394 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 19 Jan 2024 14:13:17 +0500 Subject: [PATCH 36/37] run bbim_translations.py without bpy --- src/blenderbim/Makefile | 4 +- src/blenderbim/scripts/bbim_translations.py | 483 ++++++++++---------- 2 files changed, 244 insertions(+), 243 deletions(-) diff --git a/src/blenderbim/Makefile b/src/blenderbim/Makefile index c69423b7fb..049bcadccc 100644 --- a/src/blenderbim/Makefile +++ b/src/blenderbim/Makefile @@ -662,10 +662,8 @@ endif cp -r blenderbim/libs/desktop dist/blenderbim/libs/ # generate translations module for BBIM build - # note that bpy is typically available only for current Blender python version - pip install bpy git clone https://github.com/IfcOpenShell/blenderbim-translations.git dist/working - python blenderbim/scripts/bbim_translations.py -i "dist/working" -o "dist/blenderbim/translations.py" + python blenderbim/scripts/bbim_translations.py -i "dist/working" -o "dist/blenderbim" rm -rf dist/working # Remove dependencies also bundled with Blender diff --git a/src/blenderbim/scripts/bbim_translations.py b/src/blenderbim/scripts/bbim_translations.py index a7b371ad70..802ca5be6d 100644 --- a/src/blenderbim/scripts/bbim_translations.py +++ b/src/blenderbim/scripts/bbim_translations.py @@ -1,12 +1,16 @@ -import bpy -import addon_utils +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 sys import re -import bl_i18n_utils from pathlib import Path from dataclasses import dataclass from typing import Dict @@ -22,7 +26,6 @@ bl_info = { "category": "System", } -context = bpy.context # NOTE: list of available languages in Blender can be retrieved # by the command below: # bpy.context.preferences.view.language = "test" @@ -36,7 +39,7 @@ def is_addon_loaded(addon_name) -> bool: def get_branches_directory(): - return Path(context.preferences.filepaths.i18n_branches_directory) + return Path(bpy.context.preferences.filepaths.i18n_branches_directory) def get_addon_directory() -> Path: @@ -326,266 +329,266 @@ def dump_addon_messages(module_name, do_checks, settings): return pot +if BPY_IS_LOADED: + class SetupTranslationUI(bpy.types.Operator): + bl_idname = "bim.setup_translation_ui" + bl_label = "Setup Translation UI" + bl_options = set() -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.') - 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 - addon_prefs = context.preferences.addons["ui_translate"].preferences + def is_valid_path(path: Path): + return path.is_dir() and path.is_absolute() - 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 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 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]]) - # 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 - 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}.", + ) - 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 - 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) + i18n_settings = context.window_manager.i18n_update_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" - ) + # 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" + ) - # we monkey patch `bl_i18n_utils.bl_extract_messages.dump_py_messages` - # as it's doesn't support ignoring folders - # and we need it, otherwise translation addon will try to parse strings - # from all BlenderBIM dependencies :O - bl_i18n_utils.bl_extract_messages.dump_py_messages = dump_py_messages_monkey_patch - bl_i18n_utils.bl_extract_messages.dump_addon_messages = dump_addon_messages + # we monkey patch `bl_i18n_utils.bl_extract_messages.dump_py_messages` + # as it's doesn't support ignoring folders + # and we need it, otherwise translation addon will try to parse strings + # from all BlenderBIM dependencies :O + bl_i18n_utils.bl_extract_messages.dump_py_messages = dump_py_messages_monkey_patch + bl_i18n_utils.bl_extract_messages.dump_addon_messages = dump_addon_messages - # setup selected languages - for lang in i18n_settings.langs: - lang.use = lang.uid == context.preferences.view.language + # setup selected languages + for lang in i18n_settings.langs: + lang.use = lang.uid == context.preferences.view.language - global TRANSLATION_UI_IS_LOADED - TRANSLATION_UI_IS_LOADED = True + global TRANSLATION_UI_IS_LOADED + TRANSLATION_UI_IS_LOADED = True - return {"FINISHED"} - - -class ReloadPyTranslations(bpy.types.Operator): - bl_idname = "bim.reload_py_translations" - bl_label = "Reload Py Translations" - bl_description = "Parse strings from Blender objects of the addon to `translations.py`" - bl_options = set() - use_bbim_parser: bpy.props.BoolProperty( - name="Use BlenderBIM Parser", description="As oppose to Blender parser", default=True - ) - - def execute(self, context): - if self.use_bbim_parser: - blenderbim_strings_parse() - else: - if is_addon_loaded(ADDON_NAME): - # ref: https://projects.blender.org/blender/blender/issues/116579 - raise Exception( - f"'{ADDON_NAME}' addon is enabled.\n" - "Need to disable it, restart Blender and start reloading translations again.\n" - "Otherwise some strings to translate might get lost due Blender bug." - ) - - bpy.ops.ui.i18n_addon_translation_update("INVOKE_DEFAULT", module_name=ADDON_NAME) - self.report({"INFO"}, "Translations py data is saved.") - return {"FINISHED"} - - -class ConvertTranslationsToPo(bpy.types.Operator): - bl_idname = "bim.convert_translations_to_po" - bl_label = "Convert Translations To .po" - bl_description = ( - "Extract current translation strings from translation.py to .po files and saves them to I18n Branches directory" - ) - bl_options = set() - - def execute(self, context): - temp_po_dir = tempfile.TemporaryDirectory() - branches_dir = get_branches_directory() - - bpy.ops.ui.i18n_addon_translation_export( - module_name=ADDON_NAME, directory=temp_po_dir.name, use_export_pot=True - ) - - # NOTE: we use I18n branches directory - # because it is the directory that later will be used to edit translation from UI. - # 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 exported data that way - for file in Path(temp_po_dir.name).iterdir(): - branches_subdir = branches_dir / file.stem - branches_subdir.mkdir(exist_ok=True) - file.replace(branches_subdir / file.name) - - temp_po_dir.cleanup() - self.report({"INFO"}, f"Translations .po files are saved to {branches_dir}.") - 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\n" - "back to translations.py (they also get copied to `locale` directory of the addon)" - ) - use_bbim_parser: bpy.props.BoolProperty( - name="Use BlenderBIM Parser", description="As oppose to Blender parser", default=True - ) - bl_options = set() - - def execute(self, context): - branches_dir = get_branches_directory() - if self.use_bbim_parser: - update_translations_from_po(branches_dir, get_addon_directory()) - else: - temp_po_dir = tempfile.TemporaryDirectory() - rearrange_files_for_po_import(branches_dir, temp_po_dir) - - bpy.ops.ui.i18n_addon_translation_import( - module_name=ADDON_NAME, - directory=temp_po_dir.name, - ) - - # update translations in current Blender session - 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 DisableEnableAddon(bpy.types.Operator): - bl_idname = "bim.disable_enable_addon" - bl_label = "Disable/Enable addon" - bl_description = "Will enable addon if it's disabled, will disable it and restart Blender otherwise" - bl_options = set() - - def execute(self, context): - if not is_addon_loaded(ADDON_NAME): - addon_utils.enable("blenderbim", default_set=True) return {"FINISHED"} - import os - import subprocess - addon_utils.disable("blenderbim", default_set=True) - - blender_exe = bpy.app.binary_path - head, tail = os.path.split(blender_exe) - blender_launcher = os.path.join(head, "blender-launcher.exe") - subprocess.run([blender_launcher, "-con", "--python-expr", "import bpy; bpy.ops.wm.recover_last_session()"]) - bpy.ops.wm.quit_blender() - 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") - - if not TRANSLATION_UI_IS_LOADED: - layout.operator("bim.setup_translation_ui") - layout.prop(context.preferences.filepaths, "i18n_branches_directory", text="I18n branches directory") - return - - layout.label(text="Developer UI:") - layout.operator("bim.reload_py_translations", icon="FILE_REFRESH") - - # blender restart is disabled as we'll parse strings with BBIM parser - addon_enabled = is_addon_loaded(ADDON_NAME) - row = layout.row() - row.operator( - "bim.disable_enable_addon", - icon="QUIT" if addon_enabled else "PLUGIN", - text="Disable Addon And Restart Blender" if addon_enabled else "Enable Addon", + class ReloadPyTranslations(bpy.types.Operator): + bl_idname = "bim.reload_py_translations" + bl_label = "Reload Py Translations" + bl_description = "Parse strings from Blender objects of the addon to `translations.py`" + bl_options = set() + use_bbim_parser: bpy.props.BoolProperty( + name="Use BlenderBIM Parser", description="As oppose to Blender parser", default=True ) - row.enabled = False - layout.operator("bim.convert_translations_to_po", icon="EXPORT") - layout.separator(factor=3) - layout.label(text="Translator UI:") - layout.prop(context.preferences.filepaths, "i18n_branches_directory") - layout.operator("bim.update_translations_from_po", icon="IMPORT") + def execute(self, context): + if self.use_bbim_parser: + blenderbim_strings_parse() + else: + if is_addon_loaded(ADDON_NAME): + # ref: https://projects.blender.org/blender/blender/issues/116579 + raise Exception( + f"'{ADDON_NAME}' addon is enabled.\n" + "Need to disable it, restart Blender and start reloading translations again.\n" + "Otherwise some strings to translate might get lost due Blender bug." + ) + + bpy.ops.ui.i18n_addon_translation_update("INVOKE_DEFAULT", module_name=ADDON_NAME) + self.report({"INFO"}, "Translations py data is saved.") + return {"FINISHED"} -classes = ( - ReloadPyTranslations, - ConvertTranslationsToPo, - UpdateTranslationsFromPo, - SetupTranslationUI, - DisableEnableAddon, - BBIM_PT_translations, -) + class ConvertTranslationsToPo(bpy.types.Operator): + bl_idname = "bim.convert_translations_to_po" + bl_label = "Convert Translations To .po" + bl_description = ( + "Extract current translation strings from translation.py to .po files and saves them to I18n Branches directory" + ) + bl_options = set() + + def execute(self, context): + temp_po_dir = tempfile.TemporaryDirectory() + branches_dir = get_branches_directory() + + bpy.ops.ui.i18n_addon_translation_export( + module_name=ADDON_NAME, directory=temp_po_dir.name, use_export_pot=True + ) + + # NOTE: we use I18n branches directory + # because it is the directory that later will be used to edit translation from UI. + # 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 exported data that way + for file in Path(temp_po_dir.name).iterdir(): + branches_subdir = branches_dir / file.stem + branches_subdir.mkdir(exist_ok=True) + file.replace(branches_subdir / file.name) + + temp_po_dir.cleanup() + self.report({"INFO"}, f"Translations .po files are saved to {branches_dir}.") + return {"FINISHED"} -def register(): - for cls in classes: - bpy.utils.register_class(cls) + 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\n" + "back to translations.py (they also get copied to `locale` directory of the addon)" + ) + use_bbim_parser: bpy.props.BoolProperty( + name="Use BlenderBIM Parser", description="As oppose to Blender parser", default=True + ) + bl_options = set() + + def execute(self, context): + branches_dir = get_branches_directory() + if self.use_bbim_parser: + update_translations_from_po(branches_dir, get_addon_directory()) + else: + temp_po_dir = tempfile.TemporaryDirectory() + rearrange_files_for_po_import(branches_dir, temp_po_dir) + + bpy.ops.ui.i18n_addon_translation_import( + module_name=ADDON_NAME, + directory=temp_po_dir.name, + ) + + # update translations in current Blender session + 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"} -def unregister(): - for cls in classes: - bpy.utils.unregister_class(cls) + class DisableEnableAddon(bpy.types.Operator): + bl_idname = "bim.disable_enable_addon" + bl_label = "Disable/Enable addon" + bl_description = "Will enable addon if it's disabled, will disable it and restart Blender otherwise" + bl_options = set() + + def execute(self, context): + if not is_addon_loaded(ADDON_NAME): + addon_utils.enable("blenderbim", default_set=True) + return {"FINISHED"} + + import os + import subprocess + + addon_utils.disable("blenderbim", default_set=True) + + blender_exe = bpy.app.binary_path + head, tail = os.path.split(blender_exe) + blender_launcher = os.path.join(head, "blender-launcher.exe") + subprocess.run([blender_launcher, "-con", "--python-expr", "import bpy; bpy.ops.wm.recover_last_session()"]) + bpy.ops.wm.quit_blender() + 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") + + if not TRANSLATION_UI_IS_LOADED: + layout.operator("bim.setup_translation_ui") + layout.prop(context.preferences.filepaths, "i18n_branches_directory", text="I18n branches directory") + return + + layout.label(text="Developer UI:") + layout.operator("bim.reload_py_translations", icon="FILE_REFRESH") + + # blender restart is disabled as we'll parse strings with BBIM parser + addon_enabled = is_addon_loaded(ADDON_NAME) + row = layout.row() + row.operator( + "bim.disable_enable_addon", + icon="QUIT" if addon_enabled else "PLUGIN", + text="Disable Addon And Restart Blender" if addon_enabled else "Enable Addon", + ) + row.enabled = False + + layout.operator("bim.convert_translations_to_po", icon="EXPORT") + layout.separator(factor=3) + layout.label(text="Translator UI:") + layout.prop(context.preferences.filepaths, "i18n_branches_directory") + layout.operator("bim.update_translations_from_po", icon="IMPORT") + + + classes = ( + ReloadPyTranslations, + ConvertTranslationsToPo, + UpdateTranslationsFromPo, + SetupTranslationUI, + DisableEnableAddon, + 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) def bpy_update_translations_from_po(po_directory: Path, translations_module: Path): From a8f20b2c6b0c77ac0f7f340d058071e630f73595 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Fri, 19 Jan 2024 14:22:58 +0500 Subject: [PATCH 37/37] drop bpy related translations.py<->.po stuff as we won't be using it --- src/blenderbim/scripts/bbim_translations.py | 306 ++------------------ 1 file changed, 26 insertions(+), 280 deletions(-) diff --git a/src/blenderbim/scripts/bbim_translations.py b/src/blenderbim/scripts/bbim_translations.py index 802ca5be6d..30604963ff 100644 --- a/src/blenderbim/scripts/bbim_translations.py +++ b/src/blenderbim/scripts/bbim_translations.py @@ -2,6 +2,7 @@ try: import bpy import bl_i18n_utils import addon_utils + BPY_IS_LOADED = True except ModuleNotFoundError: BPY_IS_LOADED = False @@ -12,8 +13,8 @@ import importlib import os import re from pathlib import Path -from dataclasses import dataclass -from typing import Dict +from dataclasses import dataclass, field +from typing import Dict, List, Optional bl_info = { "name": "BlenderBIM Translations", @@ -65,9 +66,9 @@ def rearrange_files_for_po_import(po_dir_path: Path, temp_directory: tempfile.Te class Message: msgid: str msgctxt: str | None - sources: list[str] + sources: Optional[List[str]] = field(default_factory=list) # mapping languages to translated strings - translations: Dict[str, str] + translations: Optional[Dict[str, str]] = field(default_factory=dict) def blenderbim_strings_parse(addon_directory=None, po_directory=None): @@ -121,7 +122,7 @@ def blenderbim_strings_parse(addon_directory=None, po_directory=None): ctx = None message = matched_dict.get(string) if message is None: - message = Message(string, ctx, []) + message = Message(string, ctx) matched_dict[string] = message elif ctx != message.msgctxt and False: print( @@ -208,128 +209,8 @@ def update_translations_from_po(po_directory: Path, translations_module: Path): fo.write("\n".join(ret)) -def dump_py_messages_monkey_patch(msgs, reports, addons, settings, addons_only=False): - ignore_addon_dirs = ["libs"] - - def _get_files(path, ignore_dirs=tuple()): - if not os.path.exists(path): - return [] - if os.path.isdir(path): - files = [] - for dpath, subdirs, fnames in os.walk(path, topdown=True, followlinks=True): - if Path(dpath) in ignore_dirs: - subdirs.clear() # skip walking through `subdirs` - continue - - for fn in fnames: - if not fn.endswith(".py"): - continue - if fn.startswith("_") and fn != "__init__.py": - continue - files.append(os.path.join(dpath, fn)) - - return files - - return [path] - - files = [] - if not addons_only: - for path in settings.CUSTOM_PY_UI_FILES: - for root in (bpy.utils.resource_path(t) for t in ("USER", "LOCAL", "SYSTEM")): - files += _get_files(os.path.join(root, path)) - - # Add all given addons. - for mod in addons: - fn = mod.__file__ - if os.path.basename(fn) == "__init__.py": - parent_dir = Path(fn).parent - ignore_dirs = [parent_dir / dpath for dpath in ignore_addon_dirs] - files += _get_files(os.path.dirname(fn), ignore_dirs=ignore_dirs) - else: - files.append(fn) - - bl_i18n_utils.bl_extract_messages.dump_py_messages_from_files(msgs, reports, sorted(files), settings) - - -def dump_addon_messages(module_name, do_checks, settings): - import datetime - import addon_utils - import bl_i18n_utils.utils as utils - from bl_i18n_utils.bl_extract_messages import ( - _gen_reports, - _gen_check_ctxt, - dump_rna_messages, - _diff_check_ctxt, - dump_py_messages, - dump_addon_bl_info, - print_info, - ) - - # Enable our addon. - ver = module_name - rev = 0 - date = datetime.datetime.now() - pot = utils.I18nMessages.gen_empty_messages( - settings.PARSER_TEMPLATE_ID, ver, rev, date, date.year, settings=settings - ) - msgs = pot.msgs - - minus_pot = utils.I18nMessages.gen_empty_messages( - settings.PARSER_TEMPLATE_ID, ver, rev, date, date.year, settings=settings - ) - minus_msgs = minus_pot.msgs - - check_ctxt = _gen_check_ctxt(settings) if do_checks else None - minus_check_ctxt = _gen_check_ctxt(settings) if do_checks else None - - # Get strings from RNA, our addon being disabled - print("D") - reports = _gen_reports(check_ctxt) - print("E") - dump_rna_messages(minus_msgs, reports, settings) - print("F") - - # Now enable our addon, and re-scan RNA. - addon = utils.enable_addons(addons={module_name})[0] - print("A") - reports["check_ctxt"] = minus_check_ctxt - print("B") - dump_rna_messages(msgs, reports, settings) - print("C") - - # and make the diff! - for key in minus_msgs: - if key != settings.PO_HEADER_KEY: - if key in msgs: - del msgs[key] - else: - # This should not happen, but some messages seem to have - # leaked on add-on unregister and register? - print(f"Key not found in msgs: {key}") - - if check_ctxt: - _diff_check_ctxt(check_ctxt, minus_check_ctxt) - - # and we are done with those! - del minus_pot - del minus_msgs - del minus_check_ctxt - - # get strings from UI layout definitions text="..." args - reports["check_ctxt"] = check_ctxt - dump_py_messages(msgs, reports, {addon}, settings, addons_only=True) - - # Get strings from the addon's bl_info - dump_addon_bl_info(msgs, reports, addon, settings) - - pot.unescape() # Strings gathered in py/C source code may contain escaped chars... - print_info(reports, pot) - - print("Finished extracting UI messages!") - - return pot - if BPY_IS_LOADED: + class SetupTranslationUI(bpy.types.Operator): bl_idname = "bim.setup_translation_ui" bl_label = "Setup Translation UI" @@ -401,140 +282,45 @@ if BPY_IS_LOADED: f" - {ui_translate_settings.BLENDER_I18N_PO_DIR}\n" ) - # we monkey patch `bl_i18n_utils.bl_extract_messages.dump_py_messages` - # as it's doesn't support ignoring folders - # and we need it, otherwise translation addon will try to parse strings - # from all BlenderBIM dependencies :O - bl_i18n_utils.bl_extract_messages.dump_py_messages = dump_py_messages_monkey_patch - bl_i18n_utils.bl_extract_messages.dump_addon_messages = dump_addon_messages - - # setup selected languages - for lang in i18n_settings.langs: - lang.use = lang.uid == context.preferences.view.language - global TRANSLATION_UI_IS_LOADED TRANSLATION_UI_IS_LOADED = True return {"FINISHED"} - - class ReloadPyTranslations(bpy.types.Operator): - bl_idname = "bim.reload_py_translations" - bl_label = "Reload Py Translations" - bl_description = "Parse strings from Blender objects of the addon to `translations.py`" - bl_options = set() - use_bbim_parser: bpy.props.BoolProperty( - name="Use BlenderBIM Parser", description="As oppose to Blender parser", default=True - ) - - def execute(self, context): - if self.use_bbim_parser: - blenderbim_strings_parse() - else: - if is_addon_loaded(ADDON_NAME): - # ref: https://projects.blender.org/blender/blender/issues/116579 - raise Exception( - f"'{ADDON_NAME}' addon is enabled.\n" - "Need to disable it, restart Blender and start reloading translations again.\n" - "Otherwise some strings to translate might get lost due Blender bug." - ) - - bpy.ops.ui.i18n_addon_translation_update("INVOKE_DEFAULT", module_name=ADDON_NAME) - self.report({"INFO"}, "Translations py data is saved.") - return {"FINISHED"} - - - class ConvertTranslationsToPo(bpy.types.Operator): - bl_idname = "bim.convert_translations_to_po" - bl_label = "Convert Translations To .po" - bl_description = ( - "Extract current translation strings from translation.py to .po files and saves them to I18n Branches directory" - ) + 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): - temp_po_dir = tempfile.TemporaryDirectory() - branches_dir = get_branches_directory() - - bpy.ops.ui.i18n_addon_translation_export( - module_name=ADDON_NAME, directory=temp_po_dir.name, use_export_pot=True - ) - - # NOTE: we use I18n branches directory - # because it is the directory that later will be used to edit translation from UI. - # 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 exported data that way - for file in Path(temp_po_dir.name).iterdir(): - branches_subdir = branches_dir / file.stem - branches_subdir.mkdir(exist_ok=True) - file.replace(branches_subdir / file.name) - - temp_po_dir.cleanup() - self.report({"INFO"}, f"Translations .po files are saved to {branches_dir}.") + 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\n" - "back to translations.py (they also get copied to `locale` directory of the addon)" - ) - use_bbim_parser: bpy.props.BoolProperty( - name="Use BlenderBIM Parser", description="As oppose to Blender parser", default=True + "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() - if self.use_bbim_parser: - update_translations_from_po(branches_dir, get_addon_directory()) - else: - temp_po_dir = tempfile.TemporaryDirectory() - rearrange_files_for_po_import(branches_dir, temp_po_dir) + update_translations_from_po(branches_dir, get_addon_directory()) - bpy.ops.ui.i18n_addon_translation_import( - module_name=ADDON_NAME, - directory=temp_po_dir.name, - ) + 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) - # update translations in current Blender session - 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 DisableEnableAddon(bpy.types.Operator): - bl_idname = "bim.disable_enable_addon" - bl_label = "Disable/Enable addon" - bl_description = "Will enable addon if it's disabled, will disable it and restart Blender otherwise" - bl_options = set() - - def execute(self, context): - if not is_addon_loaded(ADDON_NAME): - addon_utils.enable("blenderbim", default_set=True) - return {"FINISHED"} - - import os - import subprocess - - addon_utils.disable("blenderbim", default_set=True) - - blender_exe = bpy.app.binary_path - head, tail = os.path.split(blender_exe) - blender_launcher = os.path.join(head, "blender-launcher.exe") - subprocess.run([blender_launcher, "-con", "--python-expr", "import bpy; bpy.ops.wm.recover_last_session()"]) - bpy.ops.wm.quit_blender() - return {"FINISHED"} - - class BBIM_PT_translations(bpy.types.Panel): bl_label = "BlenderBIM Translations" bl_idname = "BBIM_PT_translations" @@ -545,75 +331,35 @@ if BPY_IS_LOADED: 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") - layout.prop(context.preferences.filepaths, "i18n_branches_directory", text="I18n branches directory") return layout.label(text="Developer UI:") - layout.operator("bim.reload_py_translations", icon="FILE_REFRESH") + layout.operator("bim.parse_blenderbim_strings", icon="FILE_REFRESH") - # blender restart is disabled as we'll parse strings with BBIM parser - addon_enabled = is_addon_loaded(ADDON_NAME) - row = layout.row() - row.operator( - "bim.disable_enable_addon", - icon="QUIT" if addon_enabled else "PLUGIN", - text="Disable Addon And Restart Blender" if addon_enabled else "Enable Addon", - ) - row.enabled = False - - layout.operator("bim.convert_translations_to_po", icon="EXPORT") - layout.separator(factor=3) + layout.separator() layout.label(text="Translator UI:") - layout.prop(context.preferences.filepaths, "i18n_branches_directory") layout.operator("bim.update_translations_from_po", icon="IMPORT") - classes = ( - ReloadPyTranslations, - ConvertTranslationsToPo, + ParseBlenderBIMStrings, UpdateTranslationsFromPo, SetupTranslationUI, - DisableEnableAddon, 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) -def bpy_update_translations_from_po(po_directory: Path, translations_module: Path): - from bl_i18n_utils.settings import I18nSettings - import bl_i18n_utils.utils as utils_i18n - - temp_po_dir = tempfile.TemporaryDirectory() - rearrange_files_for_po_import(po_directory, temp_po_dir) - - settings = I18nSettings() - - trans = utils_i18n.I18n(kind="PY", src=translations_module.as_posix(), settings=settings) - po_files = dict(utils_i18n.get_po_files_from_dir(temp_po_dir.name)) - - for po_uid, po_filepath in po_files.items(): - po_uid = po_uid[0] - msgs = utils_i18n.I18nMessages(uid=po_uid, kind="PO", key=po_uid, src=po_filepath, settings=settings) - if po_uid in trans.trans: - trans.trans[po_uid].merge(msgs, replace=True) - else: - trans.trans[po_uid] = msgs - - trans.write(kind="PY") - - 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"