# need to import PathfrompathlibimportPathfrom.l8n_constantsimport*importjsonimportreimportlogging# Set up logging for l8n warningslogger=logging.getLogger('l8n')logger.setLevel(logging.WARNING)# Create console handler with formattingifnotlogger.handlers:console_handler=logging.StreamHandler()console_handler.setLevel(logging.WARNING)formatter=logging.Formatter('\033[91m[L8N WARNING]\033[0m %(message)s')console_handler.setFormatter(formatter)logger.addHandler(console_handler)
[docs]defextract_plugin_root(absolute_path:str)->str:""" Extract the plugin root from an absolute path. For core plugins: everything after 'coreplugins/' For external plugins: everything after 'src/[plugin_name]/' Examples: - /files/mindroot/src/mindroot/coreplugins/chat/templates/chat.jinja2 -> chat/templates/chat.jinja2 - /some/path/src/my_plugin/templates/page.jinja2 -> my_plugin/templates/page.jinja2 """path=Path(absolute_path)parts=path.parts# Find coreplugins in the pathif'coreplugins'inparts:coreplugins_idx=parts.index('coreplugins')ifcoreplugins_idx+1<len(parts):return'/'.join(parts[coreplugins_idx+1:])# Find src/[plugin_name] pattern for external pluginsfori,partinenumerate(parts):ifpart=='src'andi+1<len(parts):# Check if this looks like a plugin (has templates, static, etc.)potential_plugin=parts[i+1]ifi+2<len(parts):# Has more path after src/plugin_namereturn'/'.join(parts[i+1:])# Fallback: return the filenamereturnpath.name
[docs]defextract_translation_keys(content:str)->set:""" Extract all __TRANSLATE_key__ placeholders from content. Args: content: Content to scan for translation keys Returns: Set of translation keys found in the content """pattern=r'__TRANSLATE_([a-z0-9_]+)__'matches=re.findall(pattern,content)returnset(matches)
[docs]defreplace_placeholders(content:str,language:str,plugin_path:str=None)->str:""" Replace __TRANSLATE_key__ placeholders with actual translations. This function now validates that ALL required translations are available for the specified language. If any translations are missing, it logs a strong warning and returns None to indicate fallback is needed. Args: content: Template content with placeholders language: Language code for translations plugin_path: Path to the localized file (used to determine which plugin's translations to use) Returns: Content with placeholders replaced by translations, or None if translations are incomplete """ifnotplugin_path:# No plugin path provided, return content unchangedlogger.warning("No plugin path provided for translation replacement.")returncontenttry:# Extract all translation keys from the contentrequired_keys=extract_translation_keys(content)ifnotrequired_keys:# No translation keys found, return content as-isreturncontent# Extract plugin name from the localized file path# Path format: .../localized_files/[coreplugins|external_plugins]/[plugin_name]/...path_parts=Path(plugin_path).parts# Find 'localized_files' in the pathif'localized_files'inpath_parts:idx=path_parts.index('localized_files')ifidx+2<len(path_parts):# Get plugin name (should be at idx+2)plugin_type=path_parts[idx+1]# 'coreplugins' or 'external_plugins'plugin_name=path_parts[idx+2]# Load translations for this plugintranslations_path=TRANSLATIONS_DIR/plugin_type/plugin_name/"translations.json"plugin_translations={}iftranslations_path.exists():try:withopen(translations_path,'r',encoding='utf-8')asf:plugin_translations=json.load(f)exceptExceptionase:logger.warning(f"Could not load translations from {translations_path}: {e}")returnNone# Fallback to original fileiflanguageinplugin_translations:translations=plugin_translations[language]# Check if ALL required translations are availablemissing_keys=required_keys-set(translations.keys())ifmissing_keys:# Some translations are missing - log strong warning and return Nonemissing_list=', '.join(sorted(missing_keys))logger.warning(f"\n"+f"="*80+"\n"+f"MISSING TRANSLATIONS DETECTED!\n"+f"Plugin: {plugin_name}\n"+f"Language: {language}\n"+f"File: {plugin_path}\n"+f"Missing keys: {missing_list}\n"+f"Falling back to original file to avoid showing placeholders.\n"+f"="*80)returnNone# Signal that fallback is needed# All translations are available - proceed with replacementdefreplace_match(match):key=match.group(1)returntranslations.get(key,match.group(0))# This shouldn't happen nowreturnre.sub(r'__TRANSLATE_([a-z0-9_]+)__',replace_match,content)else:# No translations for this languagelogger.warning(f"\n"+f"="*80+"\n"+f"NO TRANSLATIONS FOR LANGUAGE!\n"+f"Plugin: {plugin_name}\n"+f"Language: {language}\n"+f"File: {plugin_path}\n"+f"Required keys: {', '.join(sorted(required_keys))}\n"+f"Falling back to original file.\n"+f"="*80)returnNone# Signal that fallback is neededelse:logger.warning(f"Could not extract plugin info from path: {plugin_path}")returnNoneelse:logger.warning(f"Path does not contain 'localized_files': {plugin_path}")returnNoneexceptExceptionase:logger.warning(f"Error in replace_placeholders: {e}")returnNone# Fallback to original file on any error
[docs]defget_localized_file_path(original_path:str)->Path:""" Convert an original file path to its localized version path. Examples: - chat/templates/chat.jinja2 -> localized_files/coreplugins/chat/templates/chat.i18n.jinja2 - my_plugin/templates/page.jinja2 -> localized_files/external_plugins/my_plugin/templates/page.i18n.jinja2 """plugin_root=extract_plugin_root(original_path)path=Path(plugin_root)# Determine if this is a core plugin or external plugin based on the absolute pathif'coreplugins'inoriginal_path:base_dir=LOCALIZED_FILES_DIR/"coreplugins"else:base_dir=LOCALIZED_FILES_DIR/"external_plugins"# Add .i18n before the file extensionstem=path.stemsuffix=path.suffixnew_filename=f"{stem}.i18n{suffix}"localized_path=base_dir/path.parent/new_filenamereturnlocalized_path
[docs]defload_plugin_translations(plugin_path:str):"""Load translations for a specific plugin from disk."""translations_file=get_plugin_translations_path(plugin_path)iftranslations_file.exists():try:withopen(translations_file,'r',encoding='utf-8')asf:returnjson.load(f)exceptExceptionase:logger.warning(f"Could not load translations from {translations_file}: {e}")else:logger.warning(f"Translations file does not exist at {translations_file}")return{}
[docs]defget_plugin_translations_path(original_path:str)->Path:""" Get the path where translations should be stored for a given file. Example: /files/mindroot/src/mindroot/coreplugins/check_list/inject/admin.jinja2 -> translations/coreplugins/check_list/translations.json """plugin_root=extract_plugin_root(original_path)# Determine if this is a core plugin or external plugin based on the absolute pathif'coreplugins'inoriginal_path:base_dir=TRANSLATIONS_DIR/"coreplugins"else:base_dir=TRANSLATIONS_DIR/"external_plugins"# Get just the plugin name (first part of plugin_root)plugin_name=Path(plugin_root).parts[0]returnbase_dir/plugin_name/"translations.json"