| from __future__ import annotations |
|
|
| import os |
| import folder_paths |
| import glob |
| from aiohttp import web |
| import json |
| import logging |
| from functools import lru_cache |
|
|
| from utils.json_util import merge_json_recursive |
|
|
|
|
| |
| EXTRA_LOCALE_FILES = [ |
| "nodeDefs.json", |
| "commands.json", |
| "settings.json", |
| ] |
|
|
|
|
| def safe_load_json_file(file_path: str) -> dict: |
| if not os.path.exists(file_path): |
| return {} |
|
|
| try: |
| with open(file_path, "r", encoding="utf-8") as f: |
| return json.load(f) |
| except json.JSONDecodeError: |
| logging.error(f"Error loading {file_path}") |
| return {} |
|
|
|
|
| class CustomNodeManager: |
| @lru_cache(maxsize=1) |
| def build_translations(self): |
| """Load all custom nodes translations during initialization. Translations are |
| expected to be loaded from `locales/` folder. |
| |
| The folder structure is expected to be the following: |
| - custom_nodes/ |
| - custom_node_1/ |
| - locales/ |
| - en/ |
| - main.json |
| - commands.json |
| - settings.json |
| |
| returned translations are expected to be in the following format: |
| { |
| "en": { |
| "nodeDefs": {...}, |
| "commands": {...}, |
| "settings": {...}, |
| ...{other main.json keys} |
| } |
| } |
| """ |
|
|
| translations = {} |
|
|
| for folder in folder_paths.get_folder_paths("custom_nodes"): |
| |
| for custom_node_dir in sorted(glob.glob(os.path.join(folder, "*/"))): |
| locales_dir = os.path.join(custom_node_dir, "locales") |
| if not os.path.exists(locales_dir): |
| continue |
|
|
| for lang_dir in glob.glob(os.path.join(locales_dir, "*/")): |
| lang_code = os.path.basename(os.path.dirname(lang_dir)) |
|
|
| if lang_code not in translations: |
| translations[lang_code] = {} |
|
|
| |
| main_file = os.path.join(lang_dir, "main.json") |
| node_translations = safe_load_json_file(main_file) |
|
|
| |
| for extra_file in EXTRA_LOCALE_FILES: |
| extra_file_path = os.path.join(lang_dir, extra_file) |
| key = extra_file.split(".")[0] |
| json_data = safe_load_json_file(extra_file_path) |
| if json_data: |
| node_translations[key] = json_data |
|
|
| if node_translations: |
| translations[lang_code] = merge_json_recursive( |
| translations[lang_code], node_translations |
| ) |
|
|
| return translations |
|
|
| def add_routes(self, routes, webapp, loadedModules): |
|
|
| example_workflow_folder_names = ["example_workflows", "example", "examples", "workflow", "workflows"] |
|
|
| @routes.get("/workflow_templates") |
| async def get_workflow_templates(request): |
| """Returns a web response that contains the map of custom_nodes names and their associated workflow templates. The ones without templates are omitted.""" |
|
|
| files = [] |
|
|
| for folder in folder_paths.get_folder_paths("custom_nodes"): |
| for folder_name in example_workflow_folder_names: |
| pattern = os.path.join(folder, f"*/{folder_name}/*.json") |
| matched_files = glob.glob(pattern) |
| files.extend(matched_files) |
|
|
| workflow_templates_dict = ( |
| {} |
| ) |
| for file in files: |
| custom_nodes_name = os.path.basename( |
| os.path.dirname(os.path.dirname(file)) |
| ) |
| workflow_name = os.path.splitext(os.path.basename(file))[0] |
| workflow_templates_dict.setdefault(custom_nodes_name, []).append( |
| workflow_name |
| ) |
| return web.json_response(workflow_templates_dict) |
|
|
| |
| for module_name, module_dir in loadedModules: |
| for folder_name in example_workflow_folder_names: |
| workflows_dir = os.path.join(module_dir, folder_name) |
|
|
| if os.path.exists(workflows_dir): |
| if folder_name != "example_workflows": |
| logging.debug( |
| "Found example workflow folder '%s' for custom node '%s', consider renaming it to 'example_workflows'", |
| folder_name, module_name) |
|
|
| webapp.add_routes( |
| [ |
| web.static( |
| "/api/workflow_templates/" + module_name, workflows_dir |
| ) |
| ] |
| ) |
|
|
| @routes.get("/i18n") |
| async def get_i18n(request): |
| """Returns translations from all custom nodes' locales folders.""" |
| return web.json_response(self.build_translations()) |
|
|