1919"""
2020
2121from typing import Any
22+ from pathlib import Path
23+ import json
2224
2325# Keep live Plugins instances to support dynamic interactions (e.g., i18n refresh)
2426# We avoid importing BCASL types here to keep this utility decoupled.
@@ -64,4 +66,104 @@ def apply_translations(gui: Any, tr: dict) -> None:
6466 except Exception :
6567 continue
6668 except Exception :
67- pass
69+ pass
70+
71+ # --- Synchronization helpers ---
72+
73+ def _project_root () -> Path :
74+ try :
75+ # Plugins_SDK/GeneralContext/i18n.py -> project root is three parents up
76+ return Path (__file__ ).resolve ().parents [2 ]
77+ except Exception :
78+ return Path .cwd ()
79+
80+
81+ def _global_language_sync () -> str :
82+ """Return the current global language preference synchronously.
83+ Falls back to 'en' on failure.
84+ """
85+ try :
86+ from Core .i18n import get_current_language_sync # late import to avoid cycles
87+ code = get_current_language_sync () or "en"
88+ return str (code )
89+ except Exception :
90+ return "en"
91+
92+
93+ def _load_local_lang_sync (pkg_dir : Path , code : str ) -> dict :
94+ """Load a plugin-local language json from <pkg_dir>/languages/<code>.json with robust fallbacks."""
95+ try :
96+ base = pkg_dir / "languages"
97+ raw = (code or "en" ).strip ()
98+ low = raw .lower ().replace ("_" , "-" )
99+ candidates = []
100+ if raw :
101+ candidates .append (raw )
102+ if low and low not in candidates :
103+ candidates .append (low )
104+ base_code = None
105+ try :
106+ if "-" in low :
107+ base_code = low .split ("-" , 1 )[0 ]
108+ except Exception :
109+ base_code = None
110+ if base_code and base_code not in candidates :
111+ candidates .append (base_code )
112+ if "en" not in candidates :
113+ candidates .append ("en" )
114+ for c in candidates :
115+ f = base / f"{ c } .json"
116+ if f .is_file ():
117+ with open (f , encoding = "utf-8" ) as fp :
118+ data = json .load (fp )
119+ if isinstance (data , dict ):
120+ return data
121+ except Exception :
122+ pass
123+ return {"_meta" : {"code" : code or "en" , "name" : code or "en" }}
124+
125+
126+ def sync_with_global_language (gui : Any ) -> dict :
127+ """Synchronize all registered plugin instances with the current global language.
128+ - Resolves global language (Core.i18n.get_current_language_sync)
129+ - For each registered plugin instance, attempts to locate its package directory and
130+ load a local languages/<code>.json and call apply_i18n(gui, tr).
131+ - Returns the last loaded translation dict for convenience (can be ignored).
132+ Notes:
133+ - A plugin class may expose an attribute '__package_dir__' or a method 'get_package_dir()'
134+ returning a Path to its package root (containing languages/). If absent, this function
135+ attempts to infer the directory from the module of the class.
136+ """
137+ code = _global_language_sync ()
138+ last_tr : dict = {"_meta" : {"code" : code , "name" : code }}
139+ try :
140+ for pid , inst in list (INSTANCES .items ()):
141+ try :
142+ # Resolve package directory
143+ pkg_dir : Path | None = None
144+ try :
145+ if hasattr (inst , "get_package_dir" ) and callable (getattr (inst , "get_package_dir" )):
146+ pkg_dir = Path (inst .get_package_dir ())
147+ elif hasattr (inst , "__package_dir__" ):
148+ pkg_dir = Path (getattr (inst , "__package_dir__" ))
149+ else :
150+ mod = getattr (inst .__class__ , "__module__" , None )
151+ if mod :
152+ import importlib , inspect
153+ m = importlib .import_module (mod )
154+ f = Path (inspect .getfile (m ))
155+ pkg_dir = f .parent
156+ except Exception :
157+ pkg_dir = None
158+ if not pkg_dir :
159+ continue
160+ tr = _load_local_lang_sync (pkg_dir , code )
161+ last_tr = tr
162+ fn = getattr (inst , "apply_i18n" , None )
163+ if callable (fn ):
164+ fn (gui , tr )
165+ except Exception :
166+ continue
167+ except Exception :
168+ pass
169+ return last_tr
0 commit comments